diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48859b540..b3dd9d5ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,9 +39,11 @@ jobs: apt-get install -y sudo sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + submodules: true - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x - name: Configure arm64 packages @@ -73,7 +75,7 @@ jobs: rm -r publish/* mv "sourcegit.${{ matrix.runtime }}.tar" publish - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sourcegit.${{ matrix.runtime }} path: publish/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50e02dc95..3204df528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Output version string id: version run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 59720f15a..0640d19e9 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -13,12 +13,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + submodules: true - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x - name: Run formatting check - run: dotnet format --verify-no-changes + run: dotnet format --verify-no-changes src/SourceGit.csproj 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/localization-check.yml b/.github/workflows/localization-check.yml index c5970870b..76d7be77f 100644 --- a/.github/workflows/localization-check.yml +++ b/.github/workflows/localization-check.yml @@ -13,12 +13,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20.x' + node-version: '24.x' - name: Install dependencies run: npm install fs-extra@11.2.0 path@0.12.7 xml2js@0.6.2 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index d203dd2e2..0845774fb 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -15,9 +15,9 @@ jobs: runtime: [win-x64, win-arm64] steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sourcegit.${{ matrix.runtime }} path: build/SourceGit @@ -28,12 +28,12 @@ jobs: RUNTIME: ${{ matrix.runtime }} run: ./build/scripts/package.win.ps1 - name: Upload package artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: package.${{ matrix.runtime }} path: build/sourcegit_*.zip - name: Delete temp artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@v6 with: name: sourcegit.${{ matrix.runtime }} osx-app: @@ -44,9 +44,9 @@ jobs: runtime: [osx-x64, osx-arm64] steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sourcegit.${{ matrix.runtime }} path: build @@ -59,12 +59,12 @@ jobs: tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit ./build/scripts/package.osx-app.sh - name: Upload package artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: package.${{ matrix.runtime }} path: build/sourcegit_*.zip - name: Delete temp artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@v6 with: name: sourcegit.${{ matrix.runtime }} linux: @@ -76,7 +76,7 @@ jobs: runtime: [linux-x64, linux-arm64] steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download package dependencies run: | export DEBIAN_FRONTEND=noninteractive @@ -84,7 +84,7 @@ jobs: apt-get update apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils - name: Download build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sourcegit.${{ matrix.runtime }} path: build @@ -98,7 +98,7 @@ jobs: tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit ./build/scripts/package.linux.sh - name: Upload package artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: package.${{ matrix.runtime }} path: | @@ -106,6 +106,6 @@ jobs: build/sourcegit_*.deb build/sourcegit-*.rpm - name: Delete temp artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@v6 with: name: sourcegit.${{ matrix.runtime }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e61e608b0..816870a02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: contents: write steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -40,7 +40,7 @@ jobs: VERSION: ${{ needs.version.outputs.version }} run: gh release create "$TAG" -t "$VERSION" --notes-from-tag - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: package.* path: packages diff --git a/.gitignore b/.gitignore index e686a5342..5ca0482f9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.suo *.code-workspace +*.tss .DS_Store .DocumentRevisions-V100 @@ -28,6 +29,10 @@ node_modules/ package.json package-lock.json + +# Flatpak +!build/resources/flatpak + build/resources/ build/SourceGit/ build/SourceGit.app/ @@ -39,3 +44,4 @@ build/*.AppImage SourceGit.app/ build.command src/Properties/launchSettings.json +*.lscache diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..1ef4a5fcd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "depends/AvaloniaEdit"] + path = depends/AvaloniaEdit + url = https://github.com/love-linger/AvaloniaEdit.git diff --git a/LICENSE b/LICENSE index 442ce085e..4d00388b9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2025 sourcegit +Copyright (c) 2026 sourcegit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index eb370c3c6..38a7e99fa 100644 --- a/README.md +++ b/README.md @@ -58,33 +58,29 @@ * 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)) +* Using AI to generate commit message * Built-in conventional commit message helper. > [!WARNING] > **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. -## Translation Status - -You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md) - ## How to Use **To use this tool, you need to install Git(>=2.25.1) first.** You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits. -This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs. +This software creates a folder, which is platform-dependent, to store user settings, downloaded avatars and crash logs. -| OS | PATH | -|---------|-----------------------------------------------------| -| Windows | `%APPDATA%\SourceGit` | -| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` | -| macOS | `${HOME}/Library/Application Support/SourceGit` | +| OS | PATH | +|---------|-------------------------------------------| +| Windows | `%APPDATA%\SourceGit` | +| Linux | `~/.sourcegit` | +| macOS | `~/Library/Application Support/SourceGit` | > [!TIP] > * You can open this data storage directory from the main menu `Open Data Storage Directory`. -> * You can create a `data` folder next to the `SourceGit` executable to force this app to store data (user settings, downloaded avatars and crash logs) into it (Portable-Mode). Only works on Windows. +> * You can create a `data` folder next to the `SourceGit` executable to force this app to store data (user settings, downloaded avatars and crash logs) into it (Portable-Mode). Only works with Windows packages and Linux AppImages. For **Windows** users: @@ -104,15 +100,17 @@ For **Windows** users: 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. @@ -122,6 +120,7 @@ For **Linux** users: `deb` how to: ```shell + sudo mkdir -p /etc/apt/keyrings curl https://codeberg.org/api/packages/yataro/debian/repository.key | sudo tee /etc/apt/keyrings/sourcegit.asc echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc, arch=amd64,arm64] https://codeberg.org/api/packages/yataro/debian generic main" | sudo tee /etc/apt/sources.list.d/sourcegit.list sudo apt update @@ -141,11 +140,52 @@ For **Linux** users: ``` If your distribution isn't using `dnf`, please refer to the documentation of your distribution on how to add an `rpm` repository. + +* Thanks [@gadfly3173](https://github.com/gadfly3173) for providing `deb` repository, hosted on https://deb-repo.gadfly.vip + + ```shell + # Import GPG key + curl -fsSL https://deb-repo.gadfly.vip/public.key | sudo gpg --dearmor -o /usr/share/keyrings/deb-repo.gpg + + # Add repository (DEB822 format, recommended for Bookworm+ / 22.04+) + echo "Types: deb + URIs: https://deb-repo.gadfly.vip + Suites: stable + Components: main + Architectures: $(dpkg --print-architecture) + Signed-By: /usr/share/keyrings/deb-repo.gpg" | sudo tee /etc/apt/sources.list.d/deb-repo.sources + + # Or use one‑line format for older releases: + # echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/deb-repo.gpg] https://deb-repo.gadfly.vip stable main" | sudo tee / etc/apt/sources.list.d/deb-repo.list + + # Update and install + sudo apt update + sudo apt install sourcegit + ``` + +* `AOSC OS`: In addition to AMD64 and ARM64, support for LoongArch and RISC-V has been added. + ```shell + sudo oma install sourcegit + ``` + +> [!NOTE] +> RISC-V support is untested. + * `AppImage` files can be found on [AppImage hub](https://appimage.github.io/SourceGit/), `xdg-open` (`xdg-utils`) must be installed to support open native file manager. * Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) or [git-credential-libsecret](https://pkgs.org/search/?q=git-credential-libsecret) is installed on your Linux. * Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI. * If you can NOT type accented characters, such as `ê`, `ó`, try to set the environment variable `AVALONIA_IM_MODULE` to `none`. +## Commandline arguments + +Users can also launcher `SourceGit` from commandline. Usage: + +``` + // Open repository in existing `SourceGit` instance or a new one + --history // Launch `SourceGit` to see the history of a file or dir + --blame // Launch `SourceGit` to blame a file (HEAD version only) +``` + ## OpenAI This software supports using OpenAI or other AI service that has an OpenAI compatible HTTP API to generate commit message. You need configurate the service in `Preference` window. @@ -169,19 +209,24 @@ This app supports open repository in external tools listed in the table below. | Visual Studio Code - Insiders | YES | YES | YES | | VSCodium | YES | YES | YES | | Cursor | YES | YES | YES | -| Fleet | YES | YES | YES | | Sublime Text | YES | YES | YES | | Zed | YES | YES | YES | | Visual Studio | YES | NO | NO | > [!NOTE] -> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app. -> To solve this problem you can add a file named `external_editors.json` in app data storage directory and provide the path directly. For example: +> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app. +> To solve this problem you can add a file named `external_editors.json` in app data storage directory and provide the path directly. +> User can also exclude some editors by using `external_editors.json`. + +The format of `external_editors.json`: ```json { "tools": { "Visual Studio Code": "D:\\VSCode\\Code.exe" - } + }, + "excludes": [ + "Visual Studio Community 2019" + ] } ``` @@ -198,7 +243,8 @@ You can define your own conventional commit types (per-repository) by following { "Name": "New Feature", "Type": "Feature", - "Description": "Adding a new feature" + "Description": "Adding a new feature", + "PrefillShortDesc": "this is a test" }, { "Name": "Bug Fixes", @@ -213,6 +259,9 @@ You can define your own conventional commit types (per-repository) by following Everyone is welcome to submit a PR. Please make sure your PR is based on the latest `develop` branch and the target branch of PR is `develop`. +This project has a submodule in `depends/AvaloniaEdit` which is a custom fork of [Official AvaloniaEdit](https://github.com/AvaloniaUI/AvaloniaEdit). +Please make sure it is initialized - enable `--recurse-submodules` option while cloning or run `git submodule update --init` after cloned. + In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download): ```sh @@ -226,6 +275,27 @@ Thanks to all the people who contribute. [![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=20)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors) +## Translation Status + +You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md) + +### Translate Utility Script + +A script that assists with translations by reading the target language, comparing it with the base language, and going through missing keys one by one, so the translator can provide the translations interactively without needing to check each key manually. + +#### Usage + +Check for a given language (e.g., `pt_BR`) and optionally check for missing translations: + +```bash +python translate_helper.py pt_BR [--check] +``` + +- `pt_BR` is the target language code (change as needed), it should correspond to a file named `pt_BR.axaml` in the `src/Resources/Locales/` directory, so you can replace it with any other language code you want to translate, e.g., `de_DE`, `es_ES`, etc. +- `--check` is an optional flag used to only check for missing keys without prompting for translations, useful for getting a list of missing translations. + +The script will read the base language file (`en_US.axaml`) and the target language file (e.g., `pt_BR.axaml`), identify missing keys, and prompt you to provide translations for those keys. If the `--check` flag is used, it will only list the missing keys without prompting for translations. + ## Third-Party Components For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md). diff --git a/SourceGit.slnx b/SourceGit.slnx index 080f2e63e..9d6f8ab09 100644 --- a/SourceGit.slnx +++ b/SourceGit.slnx @@ -44,13 +44,18 @@ - + + + + + + diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md index 49de958a6..e2600fb4f 100644 --- a/THIRD-PARTY-LICENSES.md +++ b/THIRD-PARTY-LICENSES.md @@ -7,42 +7,43 @@ The project uses the following third-party libraries or assets ### AvaloniaUI - **Source**: https://github.com/AvaloniaUI/Avalonia -- **Version**: 11.3.9 +- **Version**: 12.0.5 - **License**: MIT License - **License Link**: https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md -### AvaloniaEdit +### Avalonia.Controls.DataGrid -- **Source**: https://github.com/AvaloniaUI/AvaloniaEdit -- **Version**: 11.3.0 +- **Source**: https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid +- **Version**: 12.0.1 - **License**: MIT License -- **License Link**: https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/LICENSE +- **License Link**: https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid/blob/master/licence.md -### LiveChartsCore.SkiaSharpView.Avalonia +### AvaloniaEdit -- **Source**: https://github.com/beto-rodriguez/LiveCharts2 -- **Version**: 2.0.0-rc6.1 +- **Official Source**: https://github.com/AvaloniaUI/AvaloniaEdit +- **Fork (Modified)**: https://github.com/love-linger/AvaloniaEdit +- **Version**: Based on 12.0.x - **License**: MIT License -- **License Link**: https://github.com/beto-rodriguez/LiveCharts2/blob/master/LICENSE +- **License Link**: https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/LICENSE ### TextMateSharp - **Source**: https://github.com/danipen/TextMateSharp -- **Version**: 1.0.70 +- **Version**: 2.0.2 - **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.5.0 +- **Version**: 2.10.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.5.0-beta.1 +- **Version**: 2.9.0-beta.1 - **License**: MIT License - **License Link**: https://github.com/Azure/azure-sdk-for-net/blob/main/LICENSE.txt @@ -56,10 +57,16 @@ 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 +### StbImageSharp + +- **Source**: https://github.com/StbSharp/StbImageSharp +- **Version**: 2.30.15 +- **License**: Public Domain + ## Fonts ### JetBrainsMono diff --git a/TRANSLATION.md b/TRANSLATION.md index e917c8659..47960c547 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,525 +6,482 @@ 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-98.80%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-89.52%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 +- Text.AIAssistant.Use +- Text.App.HideOthers +- Text.Apply.3Way +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.BranchCM.CompareWithSpecial +- Text.CheckoutBranchFromStash +- Text.CheckoutBranchFromStash.Branch +- Text.CheckoutBranchFromStash.Stash +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.Clone.Bookmark +- Text.Clone.Group +- Text.CommandPalette.Branches +- Text.CommandPalette.BranchesAndTags +- Text.CommandPalette.RepositoryActions +- Text.CommandPalette.RevisionFiles +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.CommitDetail.CollapseToBottom +- Text.CommitMessageTextBox.Column +- Text.Compare.Changes +- Text.Compare.Commits +- Text.Compare.Commits.LeftOnly +- Text.Compare.Commits.RightOnly +- Text.Compare.Commits.Tips +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.ConfigureCustomActionControls.StringFormatter +- Text.ConfigureCustomActionControls.StringFormatter.Tip +- Text.ConfigureCustomActionControls.UseFriendlyName +- Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.CopyAsPatch +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force +- Text.Diff.EmptyFile +- Text.Diff.Submodule.UncommittedChanges +- Text.Discard.IncludeModified +- Text.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase +- Text.GitFlow.StartAt +- Text.GitFlow.StartName +- Text.GotoRevisionSelector +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare +- Text.Hotkeys.Global.OpenLocalRepository +- Text.Hotkeys.Repo.CreateBranch +- Text.Hotkeys.Repo.GoToChild +- Text.Hotkeys.Repo.ToggleHistoriesDetailPanel +- Text.Init.CommandTip +- Text.Init.ErrorMessageTip +- Text.InteractiveRebase.NoVerify +- Text.Launcher.NewVersion +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.OpenLocalRepository +- Text.OpenLocalRepository.Bookmark +- Text.OpenLocalRepository.Group +- Text.OpenLocalRepository.Path +- Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels +- Text.Preferences.General.ShowRelativeTimeInGraph +- Text.Preferences.General.Use24Hours +- Text.Preferences.General.UseCompactBranchNames +- Text.Preferences.Git.UseStashAndReapplyByDefault +- Text.Rebase.NoVerify +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts +- Text.RemoteCM.EnableAutoFetch +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.StashCM.ApplyFileChanges +- Text.StashCM.Branch +- Text.SubmoduleRevisionCompare +- Text.SubmoduleRevisionCompare.OpenDetails +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges +- Text.Worktree.Branch +- Text.Worktree.Head +- Text.Worktree.Path + +
+ +### ![el__GR](https://img.shields.io/badge/el__GR-99.51%25-yellow) + +
+Missing keys in el_GR.axaml + +- Text.Launcher.NewVersion +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.WorkingCopy.FilterChanges
-### ![es__ES](https://img.shields.io/badge/es__ES-98.69%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.90%25-yellow)
Missing keys in es_ES.axaml -- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu -- 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 +- Text.WorkingCopy.FilterChanges
-### ![fr__FR](https://img.shields.io/badge/fr__FR-98.04%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-95.49%25-yellow)
Missing keys in fr_FR.axaml -- 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 +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.CopyAsPatch +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force +- Text.Diff.EmptyFile +- Text.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase +- Text.GitFlow.StartAt +- Text.GitFlow.StartName +- Text.Launcher.NewVersion +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.Preferences.General.UseCompactBranchNames +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges
-### ![id__ID](https://img.shields.io/badge/id__ID-95.87%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-95.49%25-yellow) + +
+Missing keys in he_IL.axaml + +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.CopyAsPatch +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force +- Text.Diff.EmptyFile +- Text.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase +- Text.GitFlow.StartAt +- Text.GitFlow.StartName +- Text.Launcher.NewVersion +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.Preferences.General.UseCompactBranchNames +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges + +
+ +### ![id__ID](https://img.shields.io/badge/id__ID-99.90%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.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 +- Text.WorkingCopy.FilterChanges
-### ![it__IT](https://img.shields.io/badge/it__IT-93.25%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-89.03%25-yellow)
Missing keys in it_IT.axaml -- 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.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 +- Text.AIAssistant.Use +- Text.App.HideOthers +- Text.Apply.3Way +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.BranchCM.CompareWithSpecial +- Text.ChangeCM.ResetFileTo +- Text.CheckoutBranchFromStash +- Text.CheckoutBranchFromStash.Branch +- Text.CheckoutBranchFromStash.Stash +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.Clone.Bookmark +- Text.Clone.Group +- Text.CommandPalette.Branches +- Text.CommandPalette.BranchesAndTags +- Text.CommandPalette.RepositoryActions +- Text.CommandPalette.RevisionFiles +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.CommitDetail.CollapseToBottom +- Text.CommitMessageTextBox.Column +- Text.Compare.Changes +- Text.Compare.Commits +- Text.Compare.Commits.LeftOnly +- Text.Compare.Commits.RightOnly +- Text.Compare.Commits.Tips +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.ConfigureCustomActionControls.StringFormatter +- Text.ConfigureCustomActionControls.StringFormatter.Tip +- Text.ConfigureCustomActionControls.UseFriendlyName +- Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.CopyAsPatch +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force +- Text.Diff.EmptyFile +- Text.Diff.Submodule.UncommittedChanges +- Text.Discard.IncludeModified +- Text.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase +- Text.GitFlow.StartAt +- Text.GitFlow.StartName +- Text.GotoRevisionSelector +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare +- Text.Hotkeys.Global.OpenLocalRepository +- Text.Hotkeys.Repo.CreateBranch +- Text.Hotkeys.Repo.GoToChild +- Text.Hotkeys.Repo.GoToParent +- Text.Hotkeys.Repo.ToggleHistoriesDetailPanel +- Text.Init.CommandTip +- Text.Init.ErrorMessageTip +- Text.InteractiveRebase.NoVerify +- Text.Launcher.NewVersion +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.OpenLocalRepository +- Text.OpenLocalRepository.Bookmark +- Text.OpenLocalRepository.Group +- Text.OpenLocalRepository.Path +- Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels +- Text.Preferences.General.ShowRelativeTimeInGraph +- Text.Preferences.General.Use24Hours +- Text.Preferences.General.UseCompactBranchNames +- Text.Preferences.Git.UseStashAndReapplyByDefault +- Text.Rebase.NoVerify +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts +- Text.RemoteCM.EnableAutoFetch +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.SelfUpdate.CurrentVersion +- Text.SelfUpdate.ReleaseDate +- Text.StashCM.ApplyFileChanges +- Text.StashCM.Branch +- Text.SubmoduleRevisionCompare +- Text.SubmoduleRevisionCompare.OpenDetails +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges +- Text.Worktree.Branch +- Text.Worktree.Head +- Text.Worktree.Path
-### ![ja__JP](https://img.shields.io/badge/ja__JP-74.86%25-red) +### ![ja__JP](https://img.shields.io/badge/ja__JP-99.51%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 -- Text.Bisect.Abort -- Text.Bisect.Bad -- Text.Bisect.Detecting -- 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.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 -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitMessage -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitCM.Drop -- 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.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.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 -- 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.StringValue.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.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.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.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.Preferences.Shell.Args -- Text.Preferences.Shell.Args.Tip -- Text.Pull.RecurseSubmodules -- 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 -- 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.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 -- 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.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 +- Text.Launcher.NewVersion +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.WorkingCopy.FilterChanges
-### ![ko__KR](https://img.shields.io/badge/ko__KR-96.19%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-96.96%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 +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force +- Text.Diff.EmptyFile +- Text.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase +- Text.GitFlow.StartAt +- Text.GitFlow.StartName +- Text.Launcher.NewVersion +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges
-### ![pt__BR](https://img.shields.io/badge/pt__BR-68.66%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-62.78%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 -- Text.ApplyStash.Stash -- 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.App.HideOthers +- Text.Apply.3Way +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad - Text.Blame.BlameOnPreviousRevision -- Text.BranchCM.CreatePR -- Text.BranchCM.CreatePRForUpstream -- Text.BranchCM.CustomAction -- Text.BranchCM.EditDescription -- Text.BranchCM.MergeMultiBranches -- Text.BranchCM.ResetToSelectedCommit -- Text.BranchCM.SwitchToWorktree -- Text.BranchTree.Ahead +- Text.BranchCM.CompareWithSpecial +- Text.BranchCM.InteractiveRebase.Manually - 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.ChangeCM.Merge +- Text.ChangeCM.MergeExternal +- Text.ChangeCM.ResetFileTo - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits +- Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutBranchFromStash +- Text.CheckoutBranchFromStash.Branch +- Text.CheckoutBranchFromStash.Stash +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.Clone.Bookmark +- Text.Clone.Group - Text.Clone.RecurseSubmodules +- Text.CommandPalette.Branches +- Text.CommandPalette.BranchesAndTags +- Text.CommandPalette.RepositoryActions +- Text.CommandPalette.RevisionFiles - Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyAuthorTime - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter +- Text.CommitCM.CopyCommitterTime - Text.CommitCM.CopySubject -- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -532,22 +489,26 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.InteractiveRebase.Manually - Text.CommitCM.InteractiveRebase.Reword - Text.CommitCM.InteractiveRebase.Squash -- Text.CommitCM.Merge - Text.CommitCM.MergeMultiple - Text.CommitCM.PushRevision - Text.CommitCM.Rebase - Text.CommitCM.Reset -- Text.CommitCM.Fixup - Text.CommitDetail.Changes.Count +- Text.CommitDetail.CollapseToBottom - 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.Column - Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount +- Text.Compare.Changes +- Text.Compare.Commits +- Text.Compare.Commits.LeftOnly +- Text.Compare.Commits.RightOnly +- Text.Compare.Commits.Tips - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls @@ -557,7 +518,9 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag - Text.Configure.CustomAction.WaitForExit +- Text.Configure.Git.AskBeforeAutoUpdatingSubmodules - Text.Configure.Git.ConventionalTypesOverride +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules - Text.Configure.Git.PreferredMergeMode - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.AddSampleGiteeIssue @@ -572,25 +535,33 @@ 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.StringFormatter +- Text.ConfigureCustomActionControls.StringFormatter.Tip - Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type +- Text.ConfigureCustomActionControls.UseFriendlyName - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit +- Text.ConfirmEmptyCommit.StageSelectedThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message +- Text.CopyAsPatch - Text.CopyFullPath - Text.CreateBranch.OverwriteExisting - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force - Text.DeleteMultiTags - Text.DeleteMultiTags.DeleteFromRemotes - Text.DeleteMultiTags.Tip - Text.DeleteRepositoryNode.Path - Text.DeleteRepositoryNode.TipForGroup - Text.DeleteRepositoryNode.TipForRepository +- Text.Diff.EmptyFile - Text.Diff.First - Text.Diff.Image.Blend - Text.Diff.Image.Difference @@ -600,14 +571,13 @@ This document shows the translation status of each locale file in the repository - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted +- Text.Diff.Submodule.UncommittedChanges - Text.DirHistories - Text.DirtyState.HasLocalChanges - Text.DirtyState.HasPendingPullOrPush - Text.DirtyState.UpToDate +- Text.Discard.IncludeModified - Text.Discard.IncludeUntracked -- Text.DropHead -- Text.DropHead.Commit -- Text.DropHead.NewHead - Text.EditBranchDescription - Text.EditBranchDescription.Target - Text.ExecuteCustomAction.Target @@ -615,26 +585,77 @@ This document shows the translation status of each locale file in the repository - Text.Fetch.Force - Text.FileCM.CustomAction - Text.FileCM.ResolveUsing -- Text.GitFlow.FinishWithPush +- Text.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase - Text.GitFlow.FinishWithSquash +- Text.GitFlow.StartAt +- Text.GitFlow.StartName - Text.GitLFS.Locks.UnlockAllMyLocks - Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.GotoRevisionSelector +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare - Text.Hotkeys.Global.Clone +- Text.Hotkeys.Global.OpenLocalRepository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch +- Text.Hotkeys.Repo.GoToChild +- Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette -- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Hotkeys.Repo.ToggleHistoriesDetailPanel +- Text.Init.CommandTip +- Text.Init.ErrorMessageTip - Text.InProgress.CherryPick.Head - Text.InProgress.Merge.Operating - Text.InProgress.Rebase.StoppedAt - Text.InProgress.Revert.Head +- Text.InteractiveRebase.NoVerify - Text.InteractiveRebase.ReorderTip - Text.Launcher.Commands +- Text.Launcher.NewVersion - Text.Launcher.OpenRepository - Text.Launcher.Pages - Text.Launcher.Workspaces - Text.Merge.Edit - Text.Merge.Source +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.MergeConflictEditor.AcceptBoth.MineFirst +- Text.MergeConflictEditor.AcceptBoth.TheirsFirst +- Text.MergeConflictEditor.UseBoth +- Text.MergeConflictEditor.AllResolved +- Text.MergeConflictEditor.ConflictsRemaining +- Text.MergeConflictEditor.Mine +- Text.MergeConflictEditor.NextConflict +- Text.MergeConflictEditor.PrevConflict +- Text.MergeConflictEditor.Result +- Text.MergeConflictEditor.SaveAndStage +- Text.MergeConflictEditor.Theirs +- Text.MergeConflictEditor.Title +- Text.MergeConflictEditor.UnsavedChanges +- Text.MergeConflictEditor.UseMine +- Text.MergeConflictEditor.UseTheirs +- Text.MergeConflictEditor.Undo - Text.MergeMultiple - Text.MergeMultiple.CommitChanges - Text.MergeMultiple.Strategy @@ -642,13 +663,20 @@ This document shows the translation status of each locale file in the repository - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.No - Text.Open - Text.Open.SystemDefaultEditor - Text.OpenFile +- Text.OpenLocalRepository +- Text.OpenLocalRepository.Bookmark +- Text.OpenLocalRepository.Group +- Text.OpenLocalRepository.Path - Text.PageTabBar.Tab.MoveToWorkspace - Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels - Text.Preferences.AI.ReadApiKeyFromEnv -- Text.Preferences.AI.Streaming - Text.Preferences.Appearance.EditorTabWidth - Text.Preferences.Appearance.UseAutoHideScrollBars - Text.Preferences.DiffMerge.DiffArgs @@ -659,34 +687,46 @@ This document shows the translation status of each locale file in the repository - Text.Preferences.General.EnableCompactFolders - Text.Preferences.General.ShowChangesPageByDefault - Text.Preferences.General.ShowChangesTabInCommitDetailByDefault -- Text.Preferences.General.ShowChildren +- Text.Preferences.General.ShowRelativeTimeInGraph - Text.Preferences.General.ShowTagsInGraph +- Text.Preferences.General.Use24Hours +- Text.Preferences.General.UseCompactBranchNames - Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.SSLVerify - Text.Preferences.Git.UseLibsecret +- Text.Preferences.Git.UseStashAndReapplyByDefault - Text.Preferences.Shell.Args - Text.Preferences.Shell.Args.Tip -- Text.Pull.RecurseSubmodules - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title - Text.PushToNewBranch - Text.PushToNewBranch.Title +- Text.Rebase.NoVerify +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts - Text.RemoteCM.CustomAction +- Text.RemoteCM.EnableAutoFetch - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes - Text.Repository.Dashboard - Text.Repository.FilterCommits +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary - 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.OpenAsFolder +- Text.Repository.Resolve - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath - Text.Repository.ShowDecoratedCommitsOnly @@ -698,13 +738,14 @@ This document shows the translation status of each locale file in the repository - Text.Repository.Tags.OrderByCreatorDate - Text.Repository.Tags.OrderByName - Text.Repository.Tags.Sort -- Text.Repository.UseRelativeTimeInGraph - Text.Repository.ViewLogs - Text.Repository.Visit - Text.ResetWithoutCheckout - Text.ResetWithoutCheckout.MoveTo - Text.ResetWithoutCheckout.Target - Text.ScanRepositories.UseCustomDir +- Text.SelfUpdate.CurrentVersion +- Text.SelfUpdate.ReleaseDate - Text.SetSubmoduleBranch - Text.SetSubmoduleBranch.Submodule - Text.SetSubmoduleBranch.Current @@ -715,10 +756,9 @@ 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.ApplyFileChanges +- Text.StashCM.Branch - Text.StashCM.CopyMessage - Text.StashCM.SaveAsPatch - Text.Submodule.Branch @@ -735,70 +775,92 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.SubmoduleRevisionCompare +- Text.SubmoduleRevisionCompare.OpenDetails - Text.Tag.Tagger - Text.Tag.Time +- Text.TagCM.Checkout +- Text.TagCM.CompareTwo +- Text.TagCM.CompareWith +- Text.TagCM.CompareWithHead - Text.TagCM.Copy.Message - Text.TagCM.Copy.Name - Text.TagCM.Copy.Tagger - Text.TagCM.CopyName - Text.TagCM.CustomAction - Text.TagCM.DeleteMultiple +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.ClearCommitHistories - Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.CommitToEdit - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool +- Text.WorkingCopy.Conflicts.Merge +- Text.WorkingCopy.Conflicts.MergeExternal - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.WorkingCopy.SignOff +- Text.Worktree.Branch +- Text.Worktree.Head - Text.Worktree.Open +- Text.Worktree.Path +- Text.Yes
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.89%25-yellow) - -
-Missing keys in ru_RU.axaml - -- Text.Preferences.DiffMerge.DiffArgs - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) -### ![ta__IN](https://img.shields.io/badge/ta__IN-74.97%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-64.45%25-red)
Missing keys in ta_IN.axaml +- Text.About.ReleaseDate - Text.About.ReleaseNotes - Text.AddToIgnore - Text.AddToIgnore.Pattern - Text.AddToIgnore.Storage +- Text.AIAssistant.Use - Text.App.Hide +- Text.App.HideOthers - Text.App.ShowAll +- Text.Apply.3Way +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard - 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.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.Bisect.WaitingForFirstGood +- Text.Bisect.WaitingForMark - Text.Blame.BlameOnPreviousRevision -- Text.BranchCM.CompareWithCurrent +- Text.Blame.IgnoreWhitespace +- Text.BranchCM.CompareTwo +- Text.BranchCM.CompareWith +- Text.BranchCM.CompareWithHead +- Text.BranchCM.CompareWithSpecial - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.EditDescription +- Text.BranchCM.InteractiveRebase.Manually - Text.BranchCM.ResetToSelectedCommit - Text.BranchCM.SwitchToWorktree - Text.BranchTree.Ahead @@ -810,18 +872,34 @@ This document shows the translation status of each locale file in the repository - Text.BranchTree.Tracking - Text.BranchTree.URL - Text.BranchTree.Worktree +- Text.ChangeCM.Merge +- Text.ChangeCM.MergeExternal +- Text.ChangeCM.ResetFileTo - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits +- Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutBranchFromStash +- Text.CheckoutBranchFromStash.Branch +- Text.CheckoutBranchFromStash.Stash +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.Clone.Bookmark +- Text.Clone.Group +- Text.CommandPalette.Branches +- Text.CommandPalette.BranchesAndTags +- Text.CommandPalette.RepositoryActions +- Text.CommandPalette.RevisionFiles - Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyAuthorTime - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter +- Text.CommitCM.CopyCommitterTime - Text.CommitCM.CopySubject -- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -832,15 +910,21 @@ 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.CollapseToBottom - Text.CommitDetail.Info.CopyEmail - Text.CommitDetail.Info.CopyName - Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer +- Text.CommitMessageTextBox.Column - Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount +- Text.Compare.Changes +- Text.Compare.Commits +- Text.Compare.Commits.LeftOnly +- Text.Compare.Commits.RightOnly +- Text.Compare.Commits.Tips - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls @@ -848,7 +932,9 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.Scope.File - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.AskBeforeAutoUpdatingSubmodules - Text.Configure.Git.ConventionalTypesOverride +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules - Text.Configure.Git.PreferredMergeMode - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share @@ -861,21 +947,30 @@ 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.StringFormatter +- Text.ConfigureCustomActionControls.StringFormatter.Tip - Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type +- Text.ConfigureCustomActionControls.UseFriendlyName - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit +- Text.ConfirmEmptyCommit.StageSelectedThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message +- Text.CopyAsPatch - Text.CreateBranch.OverwriteExisting +- Text.DealWithLocalChanges.DoNothing - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force - Text.DeleteMultiTags - Text.DeleteMultiTags.DeleteFromRemotes - Text.DeleteMultiTags.Tip +- Text.Diff.EmptyFile - Text.Diff.Image.Blend - Text.Diff.Image.Difference - Text.Diff.Image.SideBySide @@ -883,41 +978,99 @@ This document shows the translation status of each locale file in the repository - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted +- Text.Diff.Submodule.UncommittedChanges - Text.DirHistories - Text.DirtyState.HasLocalChanges - Text.DirtyState.HasPendingPullOrPush - Text.DirtyState.UpToDate +- Text.Discard.IncludeModified - 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.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase - Text.GitFlow.FinishWithSquash +- Text.GitFlow.StartAt +- Text.GitFlow.StartName - Text.GitLFS.Locks.UnlockAllMyLocks - Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.GotoRevisionSelector +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare +- Text.Hotkeys.Global.OpenLocalRepository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch +- Text.Hotkeys.Repo.GoToChild +- Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette -- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Hotkeys.Repo.ToggleHistoriesDetailPanel +- Text.Init.CommandTip +- Text.Init.ErrorMessageTip +- Text.InteractiveRebase.NoVerify - Text.InteractiveRebase.ReorderTip - Text.Launcher.Commands +- Text.Launcher.NewVersion - Text.Launcher.OpenRepository - Text.Launcher.Pages - Text.Launcher.Workspaces - Text.Merge.Edit +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.MergeConflictEditor.AcceptBoth.MineFirst +- Text.MergeConflictEditor.AcceptBoth.TheirsFirst +- Text.MergeConflictEditor.UseBoth +- Text.MergeConflictEditor.AllResolved +- Text.MergeConflictEditor.ConflictsRemaining +- Text.MergeConflictEditor.Mine +- Text.MergeConflictEditor.NextConflict +- Text.MergeConflictEditor.PrevConflict +- Text.MergeConflictEditor.Result +- Text.MergeConflictEditor.SaveAndStage +- Text.MergeConflictEditor.Theirs +- Text.MergeConflictEditor.Title +- Text.MergeConflictEditor.UnsavedChanges +- Text.MergeConflictEditor.UseMine +- Text.MergeConflictEditor.UseTheirs +- Text.MergeConflictEditor.Undo - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.No - Text.Open - Text.Open.SystemDefaultEditor - Text.OpenFile +- Text.OpenLocalRepository +- Text.OpenLocalRepository.Bookmark +- Text.OpenLocalRepository.Group +- Text.OpenLocalRepository.Path - Text.PageTabBar.Tab.MoveToWorkspace - Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels - Text.Preferences.AI.ReadApiKeyFromEnv - Text.Preferences.Appearance.UseAutoHideScrollBars - Text.Preferences.DiffMerge.DiffArgs @@ -927,25 +1080,38 @@ This document shows the translation status of each locale file in the repository - Text.Preferences.General.EnableCompactFolders - Text.Preferences.General.ShowChangesPageByDefault - Text.Preferences.General.ShowChangesTabInCommitDetailByDefault +- Text.Preferences.General.ShowRelativeTimeInGraph +- Text.Preferences.General.Use24Hours +- Text.Preferences.General.UseCompactBranchNames - Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.UseLibsecret +- Text.Preferences.Git.UseStashAndReapplyByDefault - Text.Preferences.Shell.Args - Text.Preferences.Shell.Args.Tip -- Text.Pull.RecurseSubmodules - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title - Text.PushToNewBranch - Text.PushToNewBranch.Title +- Text.Rebase.NoVerify +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts - Text.RemoteCM.CustomAction +- Text.RemoteCM.EnableAutoFetch - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes - Text.Repository.Dashboard +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary - Text.Repository.MoreOptions -- Text.Repository.OnlyHighlightCurrentBranchInGraph +- Text.Repository.OpenAsFolder +- Text.Repository.Resolve - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath - Text.Repository.ShowDecoratedCommitsOnly @@ -953,22 +1119,22 @@ This document shows the translation status of each locale file in the repository - 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.ScanRepositories.UseCustomDir +- Text.SelfUpdate.CurrentVersion +- Text.SelfUpdate.ReleaseDate - 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.ApplyFileChanges +- Text.StashCM.Branch - Text.StashCM.CopyMessage - Text.Submodule.Branch - Text.Submodule.CopyBranch @@ -984,58 +1150,88 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.SubmoduleRevisionCompare +- Text.SubmoduleRevisionCompare.OpenDetails - Text.Tag.Tagger - Text.Tag.Time +- Text.TagCM.Checkout +- Text.TagCM.CompareTwo +- Text.TagCM.CompareWith +- Text.TagCM.CompareWithHead - Text.TagCM.Copy.Message - Text.TagCM.Copy.Name - Text.TagCM.Copy.Tagger - Text.TagCM.CopyName - Text.TagCM.CustomAction - Text.TagCM.DeleteMultiple +- Text.TagCM.Merge - Text.UpdateSubmodules.Target +- Text.UpdateSubmodules.Recursive - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.ClearCommitHistories - Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.ConfirmCommitWithDetachedHead -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool +- Text.WorkingCopy.Conflicts.Merge +- Text.WorkingCopy.Conflicts.MergeExternal - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor +- Text.Worktree.Branch +- Text.Worktree.Head - Text.Worktree.Open +- Text.Worktree.Path +- Text.Yes
-### ![uk__UA](https://img.shields.io/badge/uk__UA-76.06%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-65.23%25-red)
Missing keys in uk_UA.axaml +- Text.About.ReleaseDate - Text.About.ReleaseNotes - Text.AddToIgnore - Text.AddToIgnore.Pattern - Text.AddToIgnore.Storage +- Text.AIAssistant.Use - Text.App.Hide +- Text.App.HideOthers - Text.App.ShowAll +- Text.Apply.3Way +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard - 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.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.Bisect.WaitingForFirstGood +- Text.Bisect.WaitingForMark - Text.Blame.BlameOnPreviousRevision +- Text.Blame.IgnoreWhitespace +- Text.BranchCM.CompareTwo +- Text.BranchCM.CompareWith +- Text.BranchCM.CompareWithHead +- Text.BranchCM.CompareWithSpecial - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.EditDescription +- Text.BranchCM.InteractiveRebase.Manually - Text.BranchCM.ResetToSelectedCommit - Text.BranchCM.SwitchToWorktree - Text.BranchTree.Ahead @@ -1047,18 +1243,34 @@ This document shows the translation status of each locale file in the repository - Text.BranchTree.Tracking - Text.BranchTree.URL - Text.BranchTree.Worktree +- Text.ChangeCM.Merge +- Text.ChangeCM.MergeExternal +- Text.ChangeCM.ResetFileTo - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits +- Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutBranchFromStash +- Text.CheckoutBranchFromStash.Branch +- Text.CheckoutBranchFromStash.Stash +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.Clone.Bookmark +- Text.Clone.Group +- Text.CommandPalette.Branches +- Text.CommandPalette.BranchesAndTags +- Text.CommandPalette.RepositoryActions +- Text.CommandPalette.RevisionFiles - Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyAuthorTime - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter +- Text.CommitCM.CopyCommitterTime - Text.CommitCM.CopySubject -- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -1069,15 +1281,21 @@ 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.CollapseToBottom - Text.CommitDetail.Info.CopyEmail - Text.CommitDetail.Info.CopyName - Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer +- Text.CommitMessageTextBox.Column - Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount +- Text.Compare.Changes +- Text.Compare.Commits +- Text.Compare.Commits.LeftOnly +- Text.Compare.Commits.RightOnly +- Text.Compare.Commits.Tips - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls @@ -1085,7 +1303,9 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.Scope.File - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.AskBeforeAutoUpdatingSubmodules - Text.Configure.Git.ConventionalTypesOverride +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share - Text.ConfigureCustomActionControls @@ -1097,18 +1317,27 @@ 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.StringFormatter +- Text.ConfigureCustomActionControls.StringFormatter.Tip - Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type +- Text.ConfigureCustomActionControls.UseFriendlyName - Text.ConfigureWorkspace.Name +- Text.ConfirmEmptyCommit.StageSelectedThenCommit - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message +- Text.CopyAsPatch - Text.CreateBranch.OverwriteExisting +- Text.DealWithLocalChanges.DoNothing - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteBranch.AskForRemote +- Text.DeleteBranch.Force - Text.DeleteMultiTags - Text.DeleteMultiTags.DeleteFromRemotes - Text.DeleteMultiTags.Tip +- Text.Diff.EmptyFile - Text.Diff.Image.Blend - Text.Diff.Image.Difference - Text.Diff.Image.SideBySide @@ -1116,41 +1345,99 @@ This document shows the translation status of each locale file in the repository - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted +- Text.Diff.Submodule.UncommittedChanges - Text.DirHistories - Text.DirtyState.HasLocalChanges - Text.DirtyState.HasPendingPullOrPush - Text.DirtyState.UpToDate +- Text.Discard.IncludeModified - 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.FileModeChange +- Text.FileModeChange.Deleted +- Text.FileModeChange.Directory +- Text.FileModeChange.Executable +- Text.FileModeChange.New +- Text.FileModeChange.Normal +- Text.FileModeChange.Submodule +- Text.FileModeChange.Symlink +- Text.FileModeChange.Unknown +- Text.GitFlow.Finish +- Text.GitFlow.FinishWithRebase - Text.GitFlow.FinishWithSquash +- Text.GitFlow.StartAt +- Text.GitFlow.StartName - Text.GitLFS.Locks.UnlockAllMyLocks - Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.GotoRevisionSelector +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare +- Text.Hotkeys.Global.OpenLocalRepository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch +- Text.Hotkeys.Repo.GoToChild +- Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette -- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Hotkeys.Repo.ToggleHistoriesDetailPanel +- Text.Init.CommandTip +- Text.Init.ErrorMessageTip +- Text.InteractiveRebase.NoVerify - Text.InteractiveRebase.ReorderTip - Text.Launcher.Commands +- Text.Launcher.NewVersion - Text.Launcher.OpenRepository - Text.Launcher.Pages - Text.Launcher.Workspaces - Text.Merge.Edit +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.MergeConflictEditor.AcceptBoth.MineFirst +- Text.MergeConflictEditor.AcceptBoth.TheirsFirst +- Text.MergeConflictEditor.UseBoth +- Text.MergeConflictEditor.AllResolved +- Text.MergeConflictEditor.ConflictsRemaining +- Text.MergeConflictEditor.Mine +- Text.MergeConflictEditor.NextConflict +- Text.MergeConflictEditor.PrevConflict +- Text.MergeConflictEditor.Result +- Text.MergeConflictEditor.SaveAndStage +- Text.MergeConflictEditor.Theirs +- Text.MergeConflictEditor.Title +- Text.MergeConflictEditor.UnsavedChanges +- Text.MergeConflictEditor.UseMine +- Text.MergeConflictEditor.UseTheirs +- Text.MergeConflictEditor.Undo - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.No - Text.Open - Text.Open.SystemDefaultEditor - Text.OpenFile +- Text.OpenLocalRepository +- Text.OpenLocalRepository.Bookmark +- Text.OpenLocalRepository.Group +- Text.OpenLocalRepository.Path - Text.PageTabBar.Tab.MoveToWorkspace - Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels - Text.Preferences.AI.ReadApiKeyFromEnv - Text.Preferences.Appearance.UseAutoHideScrollBars - Text.Preferences.DiffMerge.DiffArgs @@ -1160,25 +1447,38 @@ This document shows the translation status of each locale file in the repository - Text.Preferences.General.EnableCompactFolders - Text.Preferences.General.ShowChangesPageByDefault - Text.Preferences.General.ShowChangesTabInCommitDetailByDefault +- Text.Preferences.General.ShowRelativeTimeInGraph +- Text.Preferences.General.Use24Hours +- Text.Preferences.General.UseCompactBranchNames - Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.UseLibsecret +- Text.Preferences.Git.UseStashAndReapplyByDefault - Text.Preferences.Shell.Args - Text.Preferences.Shell.Args.Tip -- Text.Pull.RecurseSubmodules - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title - Text.PushToNewBranch - Text.PushToNewBranch.Title +- Text.Rebase.NoVerify +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts - Text.RemoteCM.CustomAction +- Text.RemoteCM.EnableAutoFetch - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes - Text.Repository.Dashboard +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary - Text.Repository.MoreOptions -- Text.Repository.OnlyHighlightCurrentBranchInGraph +- Text.Repository.OpenAsFolder +- Text.Repository.Resolve - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath - Text.Repository.ShowDecoratedCommitsOnly @@ -1186,22 +1486,22 @@ This document shows the translation status of each locale file in the repository - 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.ScanRepositories.UseCustomDir +- Text.SelfUpdate.CurrentVersion +- Text.SelfUpdate.ReleaseDate - 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.ApplyFileChanges +- Text.StashCM.Branch - Text.StashCM.CopyMessage - Text.Submodule.Branch - Text.Submodule.CopyBranch @@ -1217,26 +1517,42 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.SubmoduleRevisionCompare +- Text.SubmoduleRevisionCompare.OpenDetails - Text.Tag.Tagger - Text.Tag.Time +- Text.TagCM.Checkout +- Text.TagCM.CompareTwo +- Text.TagCM.CompareWith +- Text.TagCM.CompareWithHead - Text.TagCM.Copy.Message - Text.TagCM.Copy.Name - Text.TagCM.Copy.Tagger - Text.TagCM.CopyName - Text.TagCM.CustomAction - Text.TagCM.DeleteMultiple +- Text.TagCM.Merge +- Text.UpdateSubmodules.Recursive - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.ClearCommitHistories - Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.ConfirmCommitWithDetachedHead +- Text.WorkingCopy.Conflicts.Merge +- Text.WorkingCopy.Conflicts.MergeExternal +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor +- Text.Worktree.Branch +- Text.Worktree.Head - Text.Worktree.Open +- Text.Worktree.Path +- Text.Yes
diff --git a/VERSION b/VERSION index 1eb5f2619..054d00cdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.40 \ No newline at end of file +2026.17 \ No newline at end of file diff --git a/build/resources/_common/applications/sourcegit.desktop b/build/resources/_common/applications/sourcegit.desktop index bcf9c813c..966b1d65f 100644 --- a/build/resources/_common/applications/sourcegit.desktop +++ b/build/resources/_common/applications/sourcegit.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=SourceGit Comment=Open-source & Free Git GUI Client -Exec=/opt/sourcegit/sourcegit +Exec=/opt/sourcegit/sourcegit %u Icon=/usr/share/icons/sourcegit.png Terminal=false Type=Application diff --git a/build/resources/app/App.plist b/build/resources/app/App.plist index ba6f40a2b..d20efa5e2 100644 --- a/build/resources/app/App.plist +++ b/build/resources/app/App.plist @@ -20,6 +20,21 @@ APPL CFBundleShortVersionString SOURCE_GIT_VERSION + CFBundleDocumentTypes + + + CFBundleTypeName + Supported Folder + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.folder + + + NSHighResolutionCapable diff --git a/build/resources/appimage/sourcegit.appdata.xml b/build/resources/appimage/sourcegit.appdata.xml index 012c82d37..617737a11 100644 --- a/build/resources/appimage/sourcegit.appdata.xml +++ b/build/resources/appimage/sourcegit.appdata.xml @@ -1,16 +1,72 @@ - com.sourcegit_scm.SourceGit - MIT - MIT - SourceGit - Open-source GUI client for git users - -

Open-source GUI client for git users

-
- https://github.com/sourcegit-scm/sourcegit - com.sourcegit_scm.SourceGit.desktop - - com.sourcegit_scm.SourceGit.desktop - + com.sourcegit_scm.SourceGit + MIT + MIT + SourceGit + Open-source and Free Git GUI Client + +

Highlights

+
    +
  • Supports Windows/macOS/Linux
  • +
  • Opensource/Free
  • +
  • Fast
  • +
  • Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)/한국어
  • +
  • Built-in light/dark themes
  • +
  • Customize theme
  • +
  • Visual commit graph
  • +
  • Supports SSH access with each remote
  • +
  • GIT commands with GUI
  • +
  • Clone/Fetch/Pull/Push...
  • +
  • Merge/Rebase/Reset/Revert/Cherry-pick...
  • +
  • Amend/Reword/Squash
  • +
  • Interactive rebase
  • +
  • Branches
  • +
  • Remotes
  • +
  • Tags
  • +
  • Stashes
  • +
  • Submodules
  • +
  • Worktrees
  • +
  • Archive
  • +
  • Diff
  • +
  • Save as patch/apply
  • +
  • File histories
  • +
  • Blame
  • +
  • Revision Diffs
  • +
  • Branch Diff
  • +
  • Image Diff - Side-By-Side/Swipe/Blend
  • +
  • Git command logs
  • +
  • Search commits
  • +
  • GitFlow
  • +
  • Git LFS
  • +
  • Bisect
  • +
  • Issue Link
  • +
  • Workspace
  • +
  • Custom Action
  • +
  • Using AI to generate commit message using commitollama
  • +
+
+ https://github.com/sourcegit-scm/sourcegit/issues + https://github.com/sourcegit-scm/sourcegit/issues + https://sourcegit-scm.github.io + https://github.com/sourcegit-scm/sourcegit + com.sourcegit_scm.SourceGit.desktop + + sourcegit-scm + + + #f15336 + #f15336 + + + + https://sourcegit-scm.github.io/images/theme_dark.png + Dark Theme + + + https://sourcegit-scm.github.io/images/theme_light.png + Light Theme + + +
diff --git a/build/resources/deb/DEBIAN/control b/build/resources/deb/DEBIAN/control index 71786b435..3f944e3ab 100755 --- a/build/resources/deb/DEBIAN/control +++ b/build/resources/deb/DEBIAN/control @@ -1,7 +1,7 @@ Package: sourcegit Version: 2025.10 Priority: optional -Depends: libx11-6, libice6, libsm6, libicu | libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu69 | libicu68 | libicu67 | libicu66 | libicu65 | libicu63 | libicu60 | libicu57 | libicu55 | libicu52, xdg-utils +Depends: libx11-6, libice6, libsm6, @ICU_DEPS@, xdg-utils Architecture: amd64 Installed-Size: 60440 Maintainer: longshuang@msn.cn diff --git a/build/resources/deb/DEBIAN/postinst b/build/resources/deb/DEBIAN/postinst new file mode 100755 index 000000000..8049afa26 --- /dev/null +++ b/build/resources/deb/DEBIAN/postinst @@ -0,0 +1,33 @@ +#!/bin/sh + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ + +case "$1" in + configure) + # Update desktop database + update-desktop-database /usr/share/applications 2>/dev/null || true + # Update icon cache for hicolor theme only + gtk-update-icon-cache -f /usr/share/icons/hicolor 2>/dev/null || true + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/build/resources/flatpak/sourcegit.desktop b/build/resources/flatpak/sourcegit.desktop new file mode 100644 index 000000000..f1213e6f6 --- /dev/null +++ b/build/resources/flatpak/sourcegit.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=SourceGit +Comment=Open-source & Free Git GUI Client +Exec=sourcegit.sh +Icon=io.github.sourcegit_scm.sourcegit +Terminal=false +Type=Application +Categories=Development +MimeType=inode/directory; diff --git a/build/resources/flatpak/sourcegit.metainfo.xml b/build/resources/flatpak/sourcegit.metainfo.xml new file mode 100644 index 000000000..7601b2e7d --- /dev/null +++ b/build/resources/flatpak/sourcegit.metainfo.xml @@ -0,0 +1,73 @@ + + + io.github.sourcegit_scm.sourcegit + MIT + MIT + SourceGit + Open-source and Free Git GUI Client + +

Highlights

+
    +
  • Supports Windows/macOS/Linux
  • +
  • Opensource/Free
  • +
  • Fast
  • +
  • Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் + (Tamil)/한국어
  • +
  • Built-in light/dark themes
  • +
  • Customize theme
  • +
  • Visual commit graph
  • +
  • Supports SSH access with each remote
  • +
  • GIT commands with GUI
  • +
  • Clone/Fetch/Pull/Push...
  • +
  • Merge/Rebase/Reset/Revert/Cherry-pick...
  • +
  • Amend/Reword/Squash
  • +
  • Interactive rebase
  • +
  • Branches
  • +
  • Remotes
  • +
  • Tags
  • +
  • Stashes
  • +
  • Submodules
  • +
  • Worktrees
  • +
  • Archive
  • +
  • Diff
  • +
  • Save as patch/apply
  • +
  • File histories
  • +
  • Blame
  • +
  • Revision Diffs
  • +
  • Branch Diff
  • +
  • Image Diff - Side-By-Side/Swipe/Blend
  • +
  • Git command logs
  • +
  • Search commits
  • +
  • GitFlow
  • +
  • Git LFS
  • +
  • Bisect
  • +
  • Issue Link
  • +
  • Workspace
  • +
  • Custom Action
  • +
  • Using AI to generate commit message using commitollama
  • +
+
+ https://github.com/sourcegit-scm/sourcegit/issues + https://github.com/sourcegit-scm/sourcegit/issues + https://sourcegit-scm.github.io + https://github.com/sourcegit-scm/sourcegit + io.github.sourcegit_scm.sourcegit.desktop + + sourcegit-scm + + + #f15336 + #f15336 + + + + https://sourcegit-scm.github.io/images/theme_dark.png + Dark Theme + + + https://sourcegit-scm.github.io/images/theme_light.png + Light Theme + + + +
diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh index d9900cc48..262f141ae 100755 --- a/build/scripts/package.linux.sh +++ b/build/scripts/package.linux.sh @@ -5,6 +5,10 @@ set -o set -u set pipefail +# ICU versions to support (Debian has no virtual package, must list all) +# Format: space-separated version numbers +ICU_VERSIONS="78 77 76 74 72 71 70 69 68 67 66 65 63" + arch= appimage_arch= target= @@ -32,6 +36,7 @@ if [[ ! -f "appimagetool" ]]; then fi rm -f SourceGit/*.dbg +rm -f SourceGit/*.pdb mkdir -p SourceGit.AppDir/opt mkdir -p SourceGit.AppDir/usr/share/metainfo @@ -56,13 +61,24 @@ cp -f SourceGit/* resources/deb/opt/sourcegit ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin cp -r resources/_common/applications resources/deb/usr/share cp -r resources/_common/icons resources/deb/usr/share + # Calculate installed size in KB installed_size=$(du -sk resources/deb | cut -f1) -# Update the control file + +# Generate ICU dependencies string for Debian +# Debian lacks libicu virtual package, must list all versions with OR operator +icu_deps="libicu" +for v in $ICU_VERSIONS; do + icu_deps="$icu_deps | libicu$v" +done + +# Update the control file (replace placeholder, not whole Depends line) sed -i -e "s/^Version:.*/Version: $VERSION/" \ -e "s/^Architecture:.*/Architecture: $arch/" \ -e "s/^Installed-Size:.*/Installed-Size: $installed_size/" \ + -e "s/@ICU_DEPS@/$icu_deps/" \ resources/deb/DEBIAN/control + # Build deb package with gzip compression dpkg-deb -Zgzip --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb" diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh index 2d43e24af..8f3ddc77c 100755 --- a/build/scripts/package.osx-app.sh +++ b/build/scripts/package.osx-app.sh @@ -12,5 +12,6 @@ mv SourceGit SourceGit.app/Contents/MacOS cp resources/app/App.icns SourceGit.app/Contents/Resources/App.icns sed "s/SOURCE_GIT_VERSION/$VERSION/g" resources/app/App.plist > SourceGit.app/Contents/Info.plist rm -rf SourceGit.app/Contents/MacOS/SourceGit.dsym +rm -f SourceGit.app/Contents/MacOS/*.pdb zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit.app diff --git a/depends/AvaloniaEdit b/depends/AvaloniaEdit new file mode 160000 index 000000000..11bf2b6a8 --- /dev/null +++ b/depends/AvaloniaEdit @@ -0,0 +1 @@ +Subproject commit 11bf2b6a8265329974606604ecc96ece8bfbc0b9 diff --git a/screenshots/theme_dark.png b/screenshots/theme_dark.png index 6e8af07b3..6ecde949b 100644 Binary files a/screenshots/theme_dark.png and b/screenshots/theme_dark.png differ diff --git a/screenshots/theme_light.png b/screenshots/theme_light.png index 10988efdb..205e38d6b 100644 Binary files a/screenshots/theme_light.png and b/screenshots/theme_light.png differ diff --git a/src/AI/Agent.cs b/src/AI/Agent.cs new file mode 100644 index 000000000..a01839063 --- /dev/null +++ b/src/AI/Agent.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public class Agent + { + public Agent(Service service) + { + _service = service; + } + + public async Task GenerateCommitMessageAsync(string repo, string currentBranch, string changeList, string amendParent, Action onUpdate, CancellationToken cancellation) + { + var chatClient = _service.GetChatClient(); + if (chatClient == null) + throw new Exception("Failed to fetch available models from this service. Please check your configuration and try again."); + + var options = new ChatCompletionOptions() { Tools = { ChatTools.GetDetailChangesInFile } }; + var userMessageBuilder = new StringBuilder(); + userMessageBuilder + .AppendLine("Generate a commit message (follow the rule of conventional commit message) for given git repository.") + .AppendLine("- Read all given changed files before generating. Only binary files (such as images, audios ...) can be skipped.") + .AppendLine("- Output the conventional commit message (with detail changes in list) directly. Do not explain your output nor introduce your answer.") + .AppendLine(_service.AdditionalPrompt) + .Append("Repository path: ").AppendLine(repo.Quoted()) + .Append("Current branch: ").AppendLine(currentBranch.Quoted()) + .AppendLine("Changed files ('A' means added, 'M' means modified, 'D' means deleted, 'T' means type changed, 'R' means renamed, 'C' means copied): ") + .Append(changeList); + + var messages = new List() { new UserChatMessage(userMessageBuilder.ToString()) }; + + do + { + ChatCompletion completion = await chatClient.CompleteChatAsync(messages, options, cancellation); + var inProgress = false; + + switch (completion.FinishReason) + { + case ChatFinishReason.Stop: + if (onUpdate != null) + { + onUpdate.Invoke(string.Empty); + onUpdate.Invoke("# Assistant"); + if (completion.Content.Count > 0) + { + var text = completion.Content[0].Text.ReplaceLineEndings("\n").Trim(); + var start = 0; + var len = text.Length; + if (text.StartsWith("```", StringComparison.Ordinal)) + { + var idx = text.IndexOf('\n') + 1; + start += idx; + len -= idx; + } + + if (text.EndsWith("\n```", StringComparison.Ordinal)) + len -= 4; + + if (len > 0) + onUpdate.Invoke(text.Substring(start, len)); + else + onUpdate.Invoke("[No content was generated.]"); + } + else + { + onUpdate.Invoke("[No content was generated.]"); + } + + onUpdate.Invoke(string.Empty); + onUpdate.Invoke("# Token Usage"); + onUpdate.Invoke($"Total: {completion.Usage.TotalTokenCount}. Input: {completion.Usage.InputTokenCount}. Output: {completion.Usage.OutputTokenCount}"); + } + break; + case ChatFinishReason.Length: + throw new Exception("The response was cut off because it reached the maximum length. Consider increasing the max tokens limit."); + case ChatFinishReason.ToolCalls: + { + var message = new AssistantChatMessage(completion); +#pragma warning disable SCME0001 + var hasReasoningContent = completion.Patch.TryGetValue("$.choices[0].message.reasoning_content"u8, out string reasoning); + if (hasReasoningContent) + { + if (string.IsNullOrEmpty(reasoning)) + message.Patch.Set("$.reasoning_content"u8, BinaryData.FromString("\"\"")); + else + message.Patch.Set("$.reasoning_content"u8, reasoning); + } +#pragma warning restore SCME0001 + messages.Add(message); + + foreach (var call in completion.ToolCalls) + { + var result = await ChatTools.ProcessAsync(call, repo, amendParent, onUpdate); + messages.Add(result); + } + + inProgress = true; + break; + } + case ChatFinishReason.ContentFilter: + throw new Exception("Omitted content due to a content filter flag"); + default: + break; + } + + if (!inProgress) + break; + } while (true); + } + + private readonly Service _service; + } +} diff --git a/src/AI/ChatTools.cs b/src/AI/ChatTools.cs new file mode 100644 index 000000000..57a8e1459 --- /dev/null +++ b/src/AI/ChatTools.cs @@ -0,0 +1,53 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public static class ChatTools + { + public static readonly ChatTool GetDetailChangesInFile = ChatTool.CreateFunctionTool( + "GetDetailChangesInFile", + "Get the detailed changes in the specified file in the specified repository.", + BinaryData.FromBytes(Encoding.UTF8.GetBytes(""" + { + "type": "object", + "properties": { + "file": { + "type": "string", + "description": "The path to the file." + }, + "originalFile": { + "type": "string", + "description": "The path to the original file when it has been renamed or copied." + } + }, + "required": ["file"] + } + """)), false); + + public static async Task ProcessAsync(ChatToolCall call, string repo, string amendParent, Action output) + { + using var doc = JsonDocument.Parse(call.FunctionArguments); + + if (call.FunctionName.Equals(GetDetailChangesInFile.FunctionName)) + { + var hasFile = doc.RootElement.TryGetProperty("file", out var filePath); + var hasOriginalFile = doc.RootElement.TryGetProperty("originalFile", out var originalFilePath); + if (!hasFile) + throw new ArgumentException("file", "The file argument is required"); + + output?.Invoke($"Read changes in file: {filePath.GetString()}"); + + var orgFilePath = hasOriginalFile ? originalFilePath.GetString() : string.Empty; + var rs = await new Commands.GetFileChangeForAI(repo, filePath.GetString(), orgFilePath, amendParent).ReadAsync(); + var message = rs.IsSuccess ? rs.StdOut : string.Empty; + return new ToolChatMessage(call.Id, message); + } + + throw new NotSupportedException($"The tool {call.FunctionName} is not supported"); + } + } +} diff --git a/src/AI/Service.cs b/src/AI/Service.cs new file mode 100644 index 000000000..7d311c6c0 --- /dev/null +++ b/src/AI/Service.cs @@ -0,0 +1,98 @@ +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Azure.AI.OpenAI; +using CommunityToolkit.Mvvm.ComponentModel; +using OpenAI; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public class Service : ObservableObject + { + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + public string Server + { + get; + set; + } = string.Empty; + + public string ApiKey + { + get; + set; + } = string.Empty; + + public bool ReadApiKeyFromEnv + { + get; + set; + } = false; + + [JsonIgnore] + public List AvailableModels + { + get; + private set; + } = []; + + public string Model + { + get => _model; + set => SetProperty(ref _model, value); + } + + public bool AutoFetchAvailableModels + { + get => _autoFetchAvailableModels; + set => SetProperty(ref _autoFetchAvailableModels, value); + } + + public string AdditionalPrompt + { + get; + set; + } = string.Empty; + + public void FetchAvailableModels() + { + if (!_autoFetchAvailableModels) + { + if (!string.IsNullOrEmpty(Model)) + AvailableModels = [Model]; + return; + } + + var allModels = GetOpenAIClient().GetOpenAIModelClient().GetModels(); + AvailableModels = new List(); + foreach (var model in allModels.Value) + AvailableModels.Add(model.Id); + + if (AvailableModels.Count > 0 && (string.IsNullOrEmpty(Model) || !AvailableModels.Contains(Model))) + Model = AvailableModels[0]; + } + + public ChatClient GetChatClient() + { + return !string.IsNullOrEmpty(Model) ? GetOpenAIClient().GetChatClient(Model) : null; + } + + private OpenAIClient GetOpenAIClient() + { + var credential = new ApiKeyCredential(ReadApiKeyFromEnv ? Environment.GetEnvironmentVariable(ApiKey) : ApiKey); + return Server.Contains("openai.azure.com/", StringComparison.Ordinal) + ? new AzureOpenAIClient(new Uri(Server), credential) + : new OpenAIClient(credential, new() { Endpoint = new Uri(Server) }); + } + + private string _name = string.Empty; + private string _model = string.Empty; + private bool _autoFetchAvailableModels = true; + } +} diff --git a/src/App.Commands.cs b/src/App.Commands.cs index 194adc07c..06c0afbf1 100644 --- a/src/App.Commands.cs +++ b/src/App.Commands.cs @@ -1,7 +1,5 @@ using System; using System.Windows.Input; - -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace SourceGit @@ -39,33 +37,64 @@ public static bool IsCheckForUpdateCommandVisible } } - public static readonly Command OpenPreferencesCommand = new Command(async _ => await ShowDialog(new Views.Preferences())); - public static readonly Command OpenHotkeysCommand = new Command(async _ => await ShowDialog(new Views.Hotkeys())); - public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir)); - public static readonly Command OpenAboutCommand = new Command(async _ => await ShowDialog(new Views.About())); - public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true)); - public static readonly Command QuitCommand = new Command(_ => Quit(0)); - public static readonly Command CopyTextBlockCommand = new Command(async p => + public static readonly Command OpenPreferencesCommand = new Command(async _ => + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + var dialog = new Views.Preferences(); + await dialog.ShowDialog(owner); + } + }); + + public static readonly Command OpenHotkeysCommand = new Command(async _ => + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + var dialog = new Views.Hotkeys(); + await dialog.ShowDialog(owner); + } + }); + + public static readonly Command OpenAppDataDirCommand = new Command(_ => + { + Native.OS.OpenInFileManager(Native.OS.DataDir); + }); + + public static readonly Command OpenAboutCommand = new Command(async _ => + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + var dialog = new Views.About(); + await dialog.ShowDialog(owner); + } + }); + + public static readonly Command CheckForUpdateCommand = new Command(_ => { - if (p is not TextBlock textBlock) - return; + (Current as App)?.Check4Update(true); + }); - if (textBlock.Inlines is { Count: > 0 } inlines) - await CopyTextAsync(inlines.Text); - else if (!string.IsNullOrEmpty(textBlock.Text)) - await CopyTextAsync(textBlock.Text); + public static readonly Command QuitCommand = new Command(_ => + { + Quit(0); }); public static readonly Command HideAppCommand = new Command(_ => { - if (Current is App app && app.TryGetFeature(typeof(IActivatableLifetime)) is IActivatableLifetime lifetime) - lifetime.TryEnterBackground(); + if (OperatingSystem.IsMacOS()) + Native.MacOSUtilities.HideSelf(); + }); + + public static readonly Command HideOtherApplicationsCommand = new Command(_ => + { + if (OperatingSystem.IsMacOS()) + Native.MacOSUtilities.HideOtherApplications(); }); - public static readonly Command ShowAppCommand = new Command(_ => + public static readonly Command ShowAllApplicationsCommand = new Command(_ => { - if (Current is App app && app.TryGetFeature(typeof(IActivatableLifetime)) is IActivatableLifetime lifetime) - lifetime.TryLeaveBackground(); + if (OperatingSystem.IsMacOS()) + Native.MacOSUtilities.ShowAllApplications(); }); } } diff --git a/src/App.Extensions.cs b/src/App.Extensions.cs index d18882b86..08c1b5478 100644 --- a/src/App.Extensions.cs +++ b/src/App.Extensions.cs @@ -1,4 +1,9 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using Avalonia.Media; namespace SourceGit { @@ -13,6 +18,59 @@ public static string Escaped(this string value) { return value.Replace("\"", "\\\"", StringComparison.Ordinal); } + + public static string EscapeForBRE(this string value) + { + return value + .Replace("\\", "\\\\", StringComparison.Ordinal) + .Replace(".", "\\.", StringComparison.Ordinal) + .Replace("[", "\\[", StringComparison.Ordinal) + .Replace("*", "\\*", StringComparison.Ordinal) + .Replace("^", "\\^", StringComparison.Ordinal) + .Replace("$", "\\$", StringComparison.Ordinal) + .Replace("{", "\\{", StringComparison.Ordinal); + } + + public static string FormatFontNames(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + var parts = input.Split(','); + var trimmed = new List(); + + foreach (var part in parts) + { + var t = part.Trim(); + if (string.IsNullOrEmpty(t)) + continue; + + var sb = new StringBuilder(); + var prevChar = '\0'; + + foreach (var c in t) + { + if (c == ' ' && prevChar == ' ') + continue; + sb.Append(c); + prevChar = c; + } + + var name = sb.ToString(); + try + { + var fontFamily = FontFamily.Parse(name); + if (fontFamily.FamilyTypefaces.Count > 0) + trimmed.Add(name); + } + catch + { + // Ignore exceptions. + } + } + + return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; + } } public static class CommandExtensions @@ -22,5 +80,45 @@ public static T Use(this T cmd, Models.ICommandLog log) where T : Commands.Co cmd.Log = log; return cmd; } + + public static T WithCancellation(this T cmd, CancellationToken token) where T : Commands.Command + { + cmd.CancellationToken = token; + return cmd; + } + } + + public static class DirectoryInfoExtension + { + public static void WalkFiles(this DirectoryInfo dir, Action onFile, int maxDepth = 4) + { + try + { + var options = new EnumerationOptions() + { + IgnoreInaccessible = true, + RecurseSubdirectories = false, + }; + + foreach (var file in dir.GetFiles("*", options)) + onFile(file.FullName); + + if (maxDepth > 0) + { + foreach (var subDir in dir.GetDirectories("*", options)) + { + if (subDir.Name.StartsWith(".", StringComparison.Ordinal) || + subDir.Name.Equals("node_modules", StringComparison.OrdinalIgnoreCase)) + continue; + + WalkFiles(subDir, onFile, maxDepth - 1); + } + } + } + catch + { + // Ignore exceptions. + } + } } } diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index f19480a39..8eef67ae7 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -8,6 +9,21 @@ namespace SourceGit { + public class DateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return DateTime.ParseExact(reader.GetString(), FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime(); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToUniversalTime().ToString(FORMAT)); + } + + private const string FORMAT = "yyyy-MM-ddTHH:mm:ssZ"; + } + public class ColorConverter : JsonConverter { public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -35,40 +51,27 @@ public override void Write(Utf8JsonWriter writer, GridLength value, JsonSerializ } } - public class DataGridLengthConverter : JsonConverter - { - public override DataGridLength Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var size = reader.GetDouble(); - return new DataGridLength(size, DataGridLengthUnitType.Pixel, 0, size); - } - - public override void Write(Utf8JsonWriter writer, DataGridLength value, JsonSerializerOptions options) - { - writer.WriteNumberValue(value.DisplayValue); - } - } - [JsonSourceGenerationOptions( WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true, Converters = [ + typeof(DateTimeConverter), typeof(ColorConverter), typeof(GridLengthConverter), - typeof(DataGridLengthConverter), ] )] - [JsonSerializable(typeof(Models.ExternalToolPaths))] - [JsonSerializable(typeof(Models.HistoryFilterCollection))] + [JsonSerializable(typeof(Models.ExternalToolCustomization))] [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.RepositorySettings))] + [JsonSerializable(typeof(Models.RepositoryUIStates))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(ViewModels.Preferences))] + [JsonSerializable(typeof(ViewModels.RepositoryNodeMinimalInfo))] internal partial class JsonCodeGen : JsonSerializerContext { } } diff --git a/src/App.axaml b/src/App.axaml index a7a0c17f2..bfc05cfb4 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -12,8 +12,10 @@ + + @@ -45,7 +47,8 @@ - + + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index a247062da..a22f764d6 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,23 +1,17 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Net.Http; -using System.Reflection; -using System.Text; using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using Avalonia; 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; using Avalonia.Styling; using Avalonia.Threading; @@ -33,7 +27,7 @@ public static void Main(string[] args) AppDomain.CurrentDomain.UnhandledException += (_, e) => { - LogException(e.ExceptionObject as Exception); + Native.OS.LogException(e.ExceptionObject as Exception); }; TaskScheduler.UnobservedTaskException += (_, e) => @@ -52,7 +46,7 @@ public static void Main(string[] args) } catch (Exception ex) { - LogException(ex); + Native.OS.LogException(ex); } } @@ -77,129 +71,42 @@ public static AppBuilder BuildAvaloniaApp() Native.OS.SetupApp(builder); return builder; } - - public static void LogException(Exception ex) - { - if (ex == null) - return; - - var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); - using var writer = new StreamWriter(file); - writer.WriteLine($"Crash::: {ex.GetType().FullName}: {ex.Message}"); - writer.WriteLine(); - writer.WriteLine("----------------------------"); - writer.WriteLine($"Version: {Assembly.GetExecutingAssembly().GetName().Version}"); - writer.WriteLine($"OS: {Environment.OSVersion}"); - writer.WriteLine($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}"); - writer.WriteLine($"Source: {ex.Source}"); - writer.WriteLine($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}"); - writer.WriteLine($"User: {Environment.UserName}"); - writer.WriteLine($"App Start Time: {Process.GetCurrentProcess().StartTime}"); - writer.WriteLine($"Exception Time: {DateTime.Now}"); - writer.WriteLine($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB"); - writer.WriteLine("----------------------------"); - writer.WriteLine(); - writer.WriteLine(ex); - writer.Flush(); - } #endregion #region Utility Functions - public static Control CreateViewForViewModel(object data) - { - var dataTypeName = data.GetType().FullName; - if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal)) - return null; - - var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views."); - var viewType = Type.GetType(viewTypeName); - if (viewType != null) - return Activator.CreateInstance(viewType) as Control; - - return null; - } - - public static Task ShowDialog(object data, Window owner = null) - { - if (owner == null) - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } mainWindow }) - owner = mainWindow; - else - return null; - } - - if (data is Views.ChromelessWindow window) - return window.ShowDialog(owner); - - window = CreateViewForViewModel(data) as Views.ChromelessWindow; - if (window != null) - { - window.DataContext = data; - return window.ShowDialog(owner); - } - - return null; - } - - public static void ShowWindow(object data) - { - if (data is Views.ChromelessWindow window) - { - window.Show(); - return; - } - - window = CreateViewForViewModel(data) as Views.ChromelessWindow; - if (window != null) - { - window.DataContext = data; - window.Show(); - } - } - - public static async Task AskConfirmAsync(string message) + public static async Task AskConfirmAsync(string message, Models.ConfirmButtonType buttonType = Models.ConfirmButtonType.OkCancel) { if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) { var confirm = new Views.Confirm(); - confirm.Message.Text = message; + confirm.SetData(message, buttonType); return await confirm.ShowDialog(owner); } return false; } - public static async Task AskConfirmEmptyCommitAsync(bool hasLocalChanges) + public static async Task AskConfirmEmptyCommitAsync(bool hasLocalChanges, bool hasSelectedUnstaged) { 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; + confirm.BtnStageSelectedAndCommit.IsVisible = hasSelectedUnstaged; 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) - app._launcher.DispatchNotification(context, message, true); - } - - public static void SendNotification(string context, string message) - { - if (Current is App { _launcher: not null } app) - app._launcher.DispatchNotification(context, message, false); - } - public static void SetLocale(string localeKey) { + var locale = Models.Locale.Supported.Find(x => x.Key.Equals(localeKey, StringComparison.OrdinalIgnoreCase)); + var finalLocaleKey = locale?.Key ?? "en_US"; + if (Current is not App app || - app.Resources[localeKey] is not ResourceDictionary targetLocale || + app.Resources[finalLocaleKey] is not ResourceDictionary targetLocale || targetLocale == app._activeLocale) return; @@ -273,8 +180,8 @@ public static void SetFonts(string defaultFont, string monospaceFont) app._fontsOverrides = null; } - defaultFont = app.FixFontFamilyName(defaultFont); - monospaceFont = app.FixFontFamilyName(monospaceFont); + defaultFont = StringExtensions.FormatFontNames(defaultFont); + monospaceFont = StringExtensions.FormatFontNames(monospaceFont); var resDic = new ResourceDictionary(); if (!string.IsNullOrEmpty(defaultFont)) @@ -284,7 +191,7 @@ public static void SetFonts(string defaultFont, string monospaceFont) { if (!string.IsNullOrEmpty(defaultFont)) { - monospaceFont = $"fonts:SourceGit#JetBrains Mono,{defaultFont}"; + monospaceFont = $"fonts:SourceGit#JetBrains Mono NL,{defaultFont}"; resDic.Add("Fonts.Monospace", FontFamily.Parse(monospaceFont)); } } @@ -303,19 +210,6 @@ public static void SetFonts(string defaultFont, string monospaceFont) } } - public static async Task CopyTextAsync(string data) - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow.Clipboard: { } clipboard }) - await clipboard.SetTextAsync(data ?? ""); - } - - public static async Task GetClipboardTextAsync() - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow.Clipboard: { } clipboard }) - return await clipboard.TryGetTextAsync(); - return null; - } - public static string Text(string key, params object[] args) { var fmt = Current?.FindResource($"Text.{key}") as string; @@ -328,19 +222,6 @@ public static string Text(string key, params object[] args) return string.Format(fmt, args); } - public static Avalonia.Controls.Shapes.Path CreateMenuIcon(string key) - { - var icon = new Avalonia.Controls.Shapes.Path(); - icon.Width = 12; - icon.Height = 12; - icon.Stretch = Stretch.Uniform; - - if (Current?.FindResource(key) is StreamGeometry geo) - icon.Data = geo; - - return icon; - } - public static ViewModels.Launcher GetLauncher() { return Current is App app ? app._launcher : null; @@ -349,14 +230,9 @@ public static ViewModels.Launcher GetLauncher() public static void Quit(int exitCode) { if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - desktop.MainWindow?.Close(); desktop.Shutdown(exitCode); - } else - { Environment.Exit(exitCode); - } } #endregion @@ -366,8 +242,6 @@ public override void Initialize() AvaloniaXamlLoader.Load(this); var pref = ViewModels.Preferences.Instance; - pref.PropertyChanged += (_, _) => pref.Save(); - SetLocale(pref.Locale); SetTheme(pref.Theme, pref.ThemeOverrides); SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily); @@ -390,38 +264,21 @@ public override void OnFrameworkInitializationCompleted() if (TryLaunchAsFileHistoryViewer(desktop)) return; + if (TryLaunchAsBlameViewer(desktop)) + return; + if (TryLaunchAsCoreEditor(desktop)) return; if (TryLaunchAsAskpass(desktop)) return; - _ipcChannel = new Models.IpcChannel(); - if (!_ipcChannel.IsFirstInstance) - { - var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty; - if (!string.IsNullOrEmpty(arg)) - { - if (arg.StartsWith('"') && arg.EndsWith('"')) - arg = arg.Substring(1, arg.Length - 2).Trim(); - - if (arg.Length > 0 && !Path.IsPathFullyQualified(arg)) - arg = Path.GetFullPath(arg); - } - - _ipcChannel.SendToFirstInstance(arg); - Environment.Exit(0); - } - else - { - _ipcChannel.MessageReceived += TryOpenRepository; - desktop.Exit += (_, _) => _ipcChannel.Dispose(); - TryLaunchAsNormal(desktop); - } + TryLaunchAsNormal(desktop); } } #endregion + #region Launch Ways private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) { exitCode = -1; @@ -429,7 +286,7 @@ private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) if (args.Length <= 1 || !args[0].Equals("--rebase-todo-editor", StringComparison.Ordinal)) return false; - var file = args[1]; + var file = args[1].Replace('\\', '/').Trim('\"').Trim(); var filename = Path.GetFileName(file); if (!filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) return true; @@ -444,23 +301,7 @@ private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) using var stream = File.OpenRead(jobsFile); var collection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.InteractiveRebaseJobCollection); - using var writer = new StreamWriter(file); - foreach (var job in collection.Jobs) - { - var code = job.Action switch - { - Models.InteractiveRebaseAction.Pick => 'p', - Models.InteractiveRebaseAction.Edit => 'e', - Models.InteractiveRebaseAction.Reword => 'r', - Models.InteractiveRebaseAction.Squash => 's', - Models.InteractiveRebaseAction.Fixup => 'f', - _ => 'd' - }; - writer.WriteLine($"{code} {job.SHA}"); - } - - writer.Flush(); - + collection.WriteTodoList(file); exitCode = 0; return true; } @@ -474,7 +315,7 @@ private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCo exitCode = 0; - var file = args[1]; + var file = args[1].Replace('\\', '/').Trim('\"').Trim(); var filename = Path.GetFileName(file); if (!filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) return true; @@ -491,38 +332,64 @@ private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCo var onto = File.ReadAllText(ontoFile).Trim(); using var stream = File.OpenRead(jobsFile); var collection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.InteractiveRebaseJobCollection); - if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) - return true; + if (collection.Onto.StartsWith(onto, StringComparison.OrdinalIgnoreCase) && collection.OrigHead.StartsWith(origHead, StringComparison.OrdinalIgnoreCase)) + collection.WriteCommitMessage(doneFile, file); - var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - if (done.Length == 0) - return true; + return true; + } + + private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetime desktop) + { + var args = desktop.Args; + if (args is not { Length: > 1 } || !args[0].Equals("--history", StringComparison.Ordinal)) + return false; - var current = done[^1].Trim(); - var match = REG_REBASE_TODO().Match(current); - if (!match.Success) + var fullPath = Path.GetFullPath(args[1].Replace('\\', '/').Trim('\"').Trim()); + var dir = Path.GetDirectoryName(fullPath); + + 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 sha = match.Groups[1].Value; - foreach (var job in collection.Jobs) + Native.OS.SetupExternalTools(); + Models.AvatarManager.Instance.Start(); + + var repo = test.StdOut.Trim(); + var relativePath = Path.GetRelativePath(repo, fullPath).Replace('\\', '/'); + if (File.Exists(fullPath)) { - if (job.SHA.StartsWith(sha)) + desktop.MainWindow = new Views.FileHistories() { - File.WriteAllText(file, job.Message); - break; - } + DataContext = new ViewModels.FileHistories(repo, relativePath) + }; + } + else if (Directory.Exists(fullPath)) + { + desktop.MainWindow = new Views.DirHistories() + { + DataContext = new ViewModels.DirHistories(repo, relativePath.TrimEnd('/')) + }; + } + else + { + Console.Out.WriteLine($"'{args[1]}' does not exist in repository: '{repo}'"); + desktop.Shutdown(-1); } return true; } - private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetime desktop) + private bool TryLaunchAsBlameViewer(IClassicDesktopStyleApplicationLifetime desktop) { var args = desktop.Args; - if (args is not { Length: > 1 } || !args[0].Equals("--file-history", StringComparison.Ordinal)) + if (args is not { Length: > 1 } || !args[0].Equals("--blame", StringComparison.Ordinal)) return false; - var file = Path.GetFullPath(args[1]); + var file = Path.GetFullPath(args[1].Replace('\\', '/').Trim('\"').Trim()); var dir = Path.GetDirectoryName(file); var test = new Commands.QueryRepositoryRootPath(dir).GetResult(); @@ -534,10 +401,18 @@ private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetim } var repo = test.StdOut.Trim(); + var head = new Commands.QuerySingleCommit(repo, "HEAD").GetResult(); + if (head == null) + { + Console.Out.WriteLine($"{repo} has no commits!"); + desktop.Shutdown(-1); + return true; + } + var relFile = Path.GetRelativePath(repo, file); - var viewer = new Views.FileHistories() + var viewer = new Views.Blame() { - DataContext = new ViewModels.FileHistories(repo, relFile) + DataContext = new ViewModels.Blame(repo, relFile, head) }; desktop.MainWindow = viewer; return true; @@ -549,7 +424,7 @@ private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime deskt if (args is not { Length: > 1 } || !args[0].Equals("--core-editor", StringComparison.Ordinal)) return false; - var file = args[1]; + var file = args[1].Replace('\\', '/').Trim('\"').Trim(); if (!File.Exists(file)) { desktop.Shutdown(-1); @@ -582,56 +457,89 @@ private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) { + _ipcChannel = new Models.IpcChannel(); + if (!_ipcChannel.IsFirstInstance) + { + var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0] : string.Empty; + if (!string.IsNullOrEmpty(arg)) + { + arg = arg.Replace('\\', '/').TrimEnd('/').Trim('\"').Trim(); + if (arg.Length > 0 && !Path.IsPathFullyQualified(arg)) + arg = Path.GetFullPath(arg); + } + + _ipcChannel.SendToFirstInstance(arg); + Environment.Exit(0); + return; + } + Native.OS.SetupExternalTools(); Models.AvatarManager.Instance.Start(); + if (this.TryGetFeature() is { } activatable) + { + activatable.Activated += (_, e) => + { + if (e is FileActivatedEventArgs { Files: { Count: > 0 } } fileArgs) + _launcher?.TryOpenRepositoryFromPath(fileArgs.Files[0].Path.LocalPath); + }; + } + string startupRepo = null; - if (desktop.Args is { Length: 1 } && Directory.Exists(desktop.Args[0])) - startupRepo = desktop.Args[0]; + if (desktop.Args is { Length: 1 }) + { + var arg = desktop.Args[0].Replace('\\', '/').TrimEnd('/').Trim('\"').Trim(); + if (Directory.Exists(arg)) + startupRepo = arg; + } var pref = ViewModels.Preferences.Instance; pref.SetCanModify(); + pref.UpdateAvailableAIModels(); _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; - desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; - -#if !DISABLE_UPDATE_DETECTION - if (pref.ShouldCheck4UpdateOnStartup()) - Check4Update(); -#endif - } + desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; - private void TryOpenRepository(string repo) - { - if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) + // Fix macOS crash when quiting from Dock + if (OperatingSystem.IsMacOS()) { - var test = new Commands.QueryRepositoryRootPath(repo).GetResult(); - if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) + desktop.ShutdownRequested += (_, e) => { - Dispatcher.UIThread.Invoke(() => - { - var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); - ViewModels.Welcome.Instance.Refresh(); - _launcher?.OpenRepositoryInTab(node, null); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd }) - wnd.BringToTop(); - }); - - return; - } + e.Cancel = true; + Dispatcher.UIThread.Post(() => Quit(0)); + }; } - Dispatcher.UIThread.Invoke(() => + desktop.Exit += (_, _) => { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher }) - launcher.BringToTop(); - }); + _ipcChannel?.Dispose(); + _ipcChannel = null; + }; + + _ipcChannel.MessageReceived += repo => + { + Dispatcher.UIThread.Invoke(() => + { + _launcher.TryOpenRepositoryFromPath(repo); + if (desktop.MainWindow is Views.Launcher main) + main.BringToTop(); + }); + }; + +#if !DISABLE_UPDATE_DETECTION + if (pref.ShouldCheck4UpdateOnStartup()) + Check4Update(); +#endif } + #endregion + #region Check for Updates private void Check4Update(bool manually = false) { + if (_launcher != null) + _launcher.NewVersion = null; + Task.Run(async () => { try @@ -645,23 +553,20 @@ private void Check4Update(bool manually = false) if (ver == null) return; - // Check if already up-to-date. - if (!ver.IsNewVersion) + if (manually) { - if (manually) + if (ver.IsNewVersion) + ShowSelfUpdateResult(ver); + else ShowSelfUpdateResult(new Models.AlreadyUpToDate()); - return; } - - // Should not check ignored tag if this is called manually. - if (!manually) + else if (_launcher != null) { - var pref = ViewModels.Preferences.Instance; - if (ver.TagName == pref.IgnoreUpdateTag) + if (!ver.IsNewVersion || ver.TagName == ViewModels.Preferences.Instance.IgnoreUpdateTag) return; - } - ShowSelfUpdateResult(ver); + _launcher.NewVersion = ver; + } } catch (Exception e) { @@ -673,55 +578,24 @@ private void Check4Update(bool manually = false) private void ShowSelfUpdateResult(object data) { - Dispatcher.UIThread.Post(async () => - { - await ShowDialog(new ViewModels.SelfUpdate { Data = data }); - }); - } - - private string FixFontFamilyName(string input) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - var parts = input.Split(','); - var trimmed = new List(); - - foreach (var part in parts) + try { - var t = part.Trim(); - if (string.IsNullOrEmpty(t)) - continue; - - var sb = new StringBuilder(); - var prevChar = '\0'; - - foreach (var c in t) - { - if (c == ' ' && prevChar == ' ') - continue; - sb.Append(c); - prevChar = c; - } - - var name = sb.ToString(); - try - { - var fontFamily = FontFamily.Parse(name); - if (fontFamily.FamilyTypefaces.Count > 0) - trimmed.Add(name); - } - catch + Dispatcher.UIThread.Invoke(async () => { - // Ignore exceptions. - } + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + var ctx = new ViewModels.SelfUpdate { Data = data }; + var dialog = new Views.SelfUpdate() { DataContext = ctx }; + await dialog.ShowDialog(owner); + } + }); + } + catch + { + // Ignore exceptions. } - - return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; } - - [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")] - private static partial Regex REG_REBASE_TODO(); + #endregion private Models.IpcChannel _ipcChannel = null; private ViewModels.Launcher _launcher = null; 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/Blame.cs b/src/Commands/Blame.cs index 92753ac18..0f247d736 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -7,17 +7,22 @@ namespace SourceGit.Commands { public partial class Blame : Command { - [GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] + [GeneratedRegex(@"^\^?([0-9a-f]+)\s+(.*)\s+\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] private static partial Regex REG_FORMAT(); - public Blame(string repo, string file, string revision) + public Blame(string repo, string file, string revision, bool ignoreWhitespace) { WorkingDirectory = repo; Context = repo; - Args = $"blame -t {revision} -- {file.Quoted()}"; RaiseError = false; - _result.File = file; + var builder = new StringBuilder(); + builder.Append("blame -f -t "); + if (ignoreWhitespace) + builder.Append("-w "); + builder.Append(revision).Append(" -- ").Append(file.Quoted()); + + Args = builder.ToString(); } public async Task ReadAsync() @@ -61,19 +66,20 @@ private void ParseLine(string line) if (!match.Success) return; - _content.AppendLine(match.Groups[4].Value); + _content.AppendLine(match.Groups[5].Value); var commit = match.Groups[1].Value; - var author = match.Groups[2].Value; - var timestamp = int.Parse(match.Groups[3].Value); - var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString(_dateFormat); + var file = match.Groups[2].Value.Trim(); + var author = match.Groups[3].Value; + var timestamp = ulong.Parse(match.Groups[4].Value); var info = new Models.BlameLineInfo() { IsFirstInGroup = commit != _lastSHA, CommitSHA = commit, + File = file, Author = author, - Time = when, + Timestamp = timestamp, }; _result.LineInfos.Add(info); @@ -88,7 +94,6 @@ private void ParseLine(string line) private readonly Models.BlameData _result = new Models.BlameData(); private readonly StringBuilder _content = new StringBuilder(); - private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly; private string _lastSHA = string.Empty; private bool _needUnifyCommitSHA = false; private int _minSHALen = 64; diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index efb325d06..6ed017841 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -42,15 +42,15 @@ public async Task SetUpstreamAsync(Models.Branch tracking) return await ExecAsync().ConfigureAwait(false); } - public async Task DeleteLocalAsync() + public async Task DeleteLocalAsync(bool force) { - Args = $"branch -D {_name}"; + Args = $"branch {(force ? "-D" : "-d")} {_name}"; return await ExecAsync().ConfigureAwait(false); } - public async Task DeleteRemoteAsync(string remote) + public async Task DeleteRemoteAsync(string remote, bool force) { - Args = $"branch -D -r {remote}/{_name}"; + Args = $"branch {(force ? "-D" : "-d")} -r {remote}/{_name}"; return await ExecAsync().ConfigureAwait(false); } diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index ec3c4594c..ffd11bda9 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -14,9 +14,9 @@ public Clone(string ctx, string path, string url, string localName, string sshKe builder.Append("clone --progress --verbose "); if (!string.IsNullOrEmpty(extraArgs)) builder.Append(extraArgs).Append(' '); - builder.Append(url).Append(' '); + builder.Append(url.Quoted()).Append(' '); if (!string.IsNullOrEmpty(localName)) - builder.Append(localName); + builder.Append(localName.Quoted()); Args = builder.ToString(); } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 520f19703..977c81012 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -62,7 +62,7 @@ public async Task ExecAsync() lock (capturedLock) { if (captured is { Process: { HasExited: false } }) - captured.Process.Kill(); + captured.Process.Kill(true); } }); } @@ -70,7 +70,7 @@ public async Task ExecAsync() catch (Exception e) { if (RaiseError) - App.RaiseException(Context, e.Message); + RaiseException(e.Message); Log?.AppendLine(string.Empty); return false; @@ -101,7 +101,7 @@ public async Task ExecAsync() { var errMsg = string.Join("\n", errs).Trim(); if (!string.IsNullOrEmpty(errMsg)) - App.RaiseException(Context, errMsg); + RaiseException(errMsg); } return false; @@ -172,7 +172,7 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect) } // Force using this app as SSH askpass program - var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; + var selfExecFile = Environment.ProcessPath; start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec` start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer"); start.Environment.Add("SOURCEGIT_LAUNCH_AS_ASKPASS", "TRUE"); @@ -181,7 +181,7 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect) // 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()) @@ -219,6 +219,11 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect) return start; } + protected void RaiseException(string error) + { + Models.Notification.Send(Context, error, true); + } + private void HandleOutput(string line, List errs) { if (line == null) diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index a4ed972d6..c7ee58915 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -7,9 +7,9 @@ namespace SourceGit.Commands { public partial class CompareRevisions : Command { - [GeneratedRegex(@"^([MADC])\s+(.+)$")] + [GeneratedRegex(@"^([MADT])\s+(.+)$")] private static partial Regex REG_FORMAT(); - [GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")] + [GeneratedRegex(@"^([CR])[0-9]{0,4}\s+(.+)$")] private static partial Regex REG_RENAME_FORMAT(); public CompareRevisions(string repo, string start, string end) @@ -47,8 +47,9 @@ public CompareRevisions(string repo, string start, string end, string path) match = REG_RENAME_FORMAT().Match(line); if (match.Success) { - var renamed = new Models.Change() { Path = match.Groups[1].Value }; - renamed.Set(Models.ChangeState.Renamed); + var type = match.Groups[1].Value; + var renamed = new Models.Change() { Path = match.Groups[2].Value }; + renamed.Set(type == "R" ? Models.ChangeState.Renamed : Models.ChangeState.Copied); changes.Add(renamed); } @@ -72,8 +73,8 @@ public CompareRevisions(string repo, string start, string end, string path) change.Set(Models.ChangeState.Deleted); changes.Add(change); break; - case 'C': - change.Set(Models.ChangeState.Copied); + case 'T': + change.Set(Models.ChangeState.TypeChanged); changes.Add(change); break; } diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index 52fc021cb..75f4d738a 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading.Tasks; namespace SourceGit.Commands @@ -53,7 +54,11 @@ public async Task> ReadAllAsync() { var parts = line.Split('=', 2); if (parts.Length == 2) - rs[parts[0]] = parts[1]; + { + var key = parts[0].ToLower(CultureInfo.CurrentCulture); // Always use lower case for key + var value = parts[1]; + rs[key] = value; + } } } diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 3fdc62259..89248ddc1 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -12,14 +13,29 @@ public partial class Diff : Command [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] private static partial Regex REG_INDICATOR(); - [GeneratedRegex(@"^index\s([0-9a-f]{6,40})\.\.([0-9a-f]{6,40})(\s[1-9]{6})?")] + [GeneratedRegex(@"^index\s([0-9a-f]{6,64})\.\.([0-9a-f]{6,64})(\s[1-9]{6})?")] private static partial Regex REG_HASH_CHANGE(); - private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/"; - private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/"; - private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/"; + private const char PREFIX_CONTEXT = ' '; + private const char PREFIX_DELETED = '-'; + private const char PREFIX_ADDED = '+'; + private const char PREFIX_COMMAND = '\\'; - public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespace) + private const string FILE_MODE_OLD = "old mode "; + private const string FILE_MODE_NEW = "new mode "; + private const string FILE_MODE_DELETED = "deleted file mode "; + private const string FILE_MODE_ADDED = "new file mode "; + + private const string LFS_SPECIFIER = "version https://git-lfs.github.com/spec/"; + private const string LFS_OID_PREFIX = "oid sha256:"; + private const string LFS_SIZE_PREFIX = "size "; + + private const string SPECIAL_DIFF_START = "diff "; + private const string SPECIAL_BINARY = "Binary files "; + private const string SPECIAL_NO_NEWLINE = " No newline at end of file"; + private const string SPECIAL_SUBMODULE = "Subproject commit "; + + public Diff(string repo, Models.DiffOption opt, int numContextLines, bool ignoreWhitespace, bool ignoreCRAtEOL) { _result.TextDiff = new Models.TextDiff(); @@ -27,12 +43,12 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa 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 "); + builder.Append("diff --no-color --no-ext-diff --full-index --patch "); if (ignoreWhitespace) - builder.Append("--ignore-space-change "); - builder.Append("--unified=").Append(unified).Append(' '); + builder.Append("--ignore-space-change --ignore-blank-lines "); + if (ignoreCRAtEOL) + builder.Append("--ignore-cr-at-eol "); + builder.Append("--unified=").Append(numContextLines).Append(' '); builder.Append(opt.ToString()); Args = builder.ToString(); @@ -46,189 +62,261 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa proc.StartInfo = CreateGitStartInfo(true); proc.Start(); - while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) - ParseLine(line); + using var ms = new MemoryStream(); + await proc.StandardOutput.BaseStream.CopyToAsync(ms, CancellationToken).ConfigureAwait(false); + + if (ms.TryGetBuffer(out var buffer)) + { + var start = buffer.Offset; + var end = buffer.Offset + buffer.Count; + while (start < end) + { + var lineEnd = Array.IndexOf(buffer.Array, (byte)'\n', start); + if (lineEnd < 0) + { + ParseLine(buffer[start..]); + break; + } + + ParseLine(buffer[start..lineEnd]); + if (_result.IsBinary) + break; + + start = lineEnd + 1; + } + } - await proc.WaitForExitAsync().ConfigureAwait(false); + await proc.WaitForExitAsync(CancellationToken).ConfigureAwait(false); } catch { // Ignore exceptions. } - if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0) + if (_isLFS || _result.IsBinary || _result.TextDiff.Lines.Count == 0) { _result.TextDiff = null; } else { - ProcessInlineHighlights(); + if (_isInChunk) + { + ProcessInlineHighlights(); + _isInChunk = false; + } + + if (_result.TextDiff.Lines.Count < 4) + { + var isSubmoduleChange = true; + + for (int i = 1; i < _result.TextDiff.Lines.Count; i++) + { + var line = _result.TextDiff.Lines[i]; + if (!line.Content.StartsWith(SPECIAL_SUBMODULE, StringComparison.Ordinal)) + { + isSubmoduleChange = false; + break; + } + } + + if (isSubmoduleChange) + { + _result.IsSubmoduleChange = true; + _result.TextDiff = null; + return _result; + } + } + _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); + _result.TextDiff.OldMode = _result.OldMode; + _result.TextDiff.NewMode = _result.NewMode; + _result.TextDiff.OldHash = _result.OldHash; + _result.TextDiff.NewHash = _result.NewHash; } return _result; } - private void ParseLine(string line) + private void ParseLine(ArraySegment lineBytes) { - if (_result.IsBinary) + // Decode line bytes to UTF-8 string + var line = Encoding.UTF8.GetString(lineBytes.Array, lineBytes.Offset, lineBytes.Count); + if (line.Length == 0) return; - if (line.StartsWith("old mode ", StringComparison.Ordinal)) + // If we are reading a chunk-body, try to read the current line as chunk-body first (because + // there are usually more chunk-body lines than chunk-indicator lines). + if (_isInChunk) { - _result.OldMode = line.Substring(9); - return; + if (ParseChunkBodyLine(line, lineBytes)) + return; + + ProcessInlineHighlights(); + _isInChunk = false; } - if (line.StartsWith("new mode ", StringComparison.Ordinal)) + // If the current line is not a chunk-body, try to parse it as chunk-indicator + if (ParseChunkStartLine(line)) { - _result.NewMode = line.Substring(9); + _isInChunk = true; return; } - if (line.StartsWith("deleted file mode ", StringComparison.Ordinal)) + // Fallback to diff headers to support type-changed diff (multiple headers). + ParseDiffHeaderLine(line); + } + + private void ParseDiffHeaderLine(string line) + { + if (line.StartsWith(SPECIAL_DIFF_START, StringComparison.Ordinal)) + return; + + if (ParseFileModeChange(line)) + return; + + var match = REG_HASH_CHANGE().Match(line); + if (match.Success) { - _result.OldMode = line.Substring(18); + if (string.IsNullOrEmpty(_result.OldHash)) + _result.OldHash = match.Groups[1].Value; + _result.NewHash = match.Groups[2].Value; return; } - if (line.StartsWith("new file mode ", StringComparison.Ordinal)) + if (line.StartsWith(SPECIAL_BINARY, StringComparison.Ordinal)) + _result.IsBinary = true; + } + + private bool ParseChunkStartLine(string line) + { + var match = REG_INDICATOR().Match(line); + if (match.Success) { - _result.NewMode = line.Substring(14); - return; + _oldLine = int.Parse(match.Groups[1].Value); + _newLine = int.Parse(match.Groups[2].Value); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, null, 0, 0); + _result.TextDiff.Lines.Add(_last); + return true; } - if (_result.IsLFS) + return false; + } + + private bool ParseChunkBodyLine(string line, ArraySegment lineBytes) + { + var prefix = line[0]; + var content = line.Substring(1); + var rawContent = lineBytes[1..].ToArray(); + if (ParseLFSChange(prefix, content)) + return true; + + if (prefix == PREFIX_DELETED) { - var ch = line[0]; - if (ch == '-') - { - if (line.StartsWith("-oid sha256:", StringComparison.Ordinal)) - { - _result.LFSDiff.Old.Oid = line.Substring(12); - } - else if (line.StartsWith("-size ", StringComparison.Ordinal)) - { - _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); - } - } - else if (ch == '+') - { - if (line.StartsWith("+oid sha256:", StringComparison.Ordinal)) - { - _result.LFSDiff.New.Oid = line.Substring(12); - } - else if (line.StartsWith("+size ", StringComparison.Ordinal)) - { - _result.LFSDiff.New.Size = long.Parse(line.AsSpan(6)); - } - } - else if (line.StartsWith(" size ", StringComparison.Ordinal)) - { - _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); - } - return; + _result.TextDiff.DeletedLines++; + _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, content, rawContent, _oldLine, 0); + _deleted.Add(_last); + _oldLine++; + return true; } - if (_result.TextDiff.Lines.Count == 0) + if (prefix == PREFIX_ADDED) { - if (line.StartsWith("Binary", StringComparison.Ordinal)) - { - _result.IsBinary = true; - return; - } + _result.TextDiff.AddedLines++; + _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, content, rawContent, 0, _newLine); + _added.Add(_last); + _newLine++; + return true; + } - if (string.IsNullOrEmpty(_result.OldHash)) - { - var match = REG_HASH_CHANGE().Match(line); - if (!match.Success) - return; + if (prefix == PREFIX_CONTEXT) + { + ProcessInlineHighlights(); - _result.OldHash = match.Groups[1].Value; - _result.NewHash = match.Groups[2].Value; - } - else - { - var match = REG_INDICATOR().Match(line); - if (!match.Success) - return; - - _oldLine = int.Parse(match.Groups[1].Value); - _newLine = int.Parse(match.Groups[2].Value); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); - _result.TextDiff.Lines.Add(_last); - } + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, content, rawContent, _oldLine, _newLine); + _result.TextDiff.Lines.Add(_last); + _oldLine++; + _newLine++; + return true; } - else + + if (prefix == PREFIX_COMMAND) { - if (line.Length == 0) - { - ProcessInlineHighlights(); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine); - _result.TextDiff.Lines.Add(_last); - _oldLine++; - _newLine++; - return; - } + if (content.Equals(SPECIAL_NO_NEWLINE, StringComparison.Ordinal)) + _last.NoNewLineEndOfFile = true; + return true; + } - var ch = line[0]; - if (ch == '-') - { - if (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal)) - { - _result.IsLFS = true; - _result.LFSDiff = new Models.LFSDiff(); - return; - } + return false; + } + + private bool ParseFileModeChange(string line) + { + if (line.StartsWith(FILE_MODE_OLD, StringComparison.Ordinal)) + { + _result.OldMode = int.Parse(line.AsSpan(9)); + return true; + } + + if (line.StartsWith(FILE_MODE_NEW, StringComparison.Ordinal)) + { + _result.NewMode = int.Parse(line.AsSpan(9)); + return true; + } + + if (line.StartsWith(FILE_MODE_DELETED, StringComparison.Ordinal)) + { + _result.OldMode = int.Parse(line.AsSpan(18)); + return true; + } + + if (line.StartsWith(FILE_MODE_ADDED, StringComparison.Ordinal)) + { + _result.NewMode = int.Parse(line.AsSpan(14)); + return true; + } + + return false; + } - _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); - _deleted.Add(_last); - _oldLine++; + private bool ParseLFSChange(char prefix, string content) + { + if (_isLFS) + { + if (prefix == PREFIX_DELETED) + { + if (content.StartsWith(LFS_OID_PREFIX, StringComparison.Ordinal)) + _result.LFSDiff.Old.Oid = content.Substring(11); + else if (content.StartsWith(LFS_SIZE_PREFIX, StringComparison.Ordinal)) + _result.LFSDiff.Old.Size = long.Parse(content.AsSpan(5)); } - else if (ch == '+') + else if (prefix == PREFIX_ADDED) { - if (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal)) - { - _result.IsLFS = true; - _result.LFSDiff = new Models.LFSDiff(); - return; - } - - _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine); - _added.Add(_last); - _newLine++; + if (content.StartsWith(LFS_OID_PREFIX, StringComparison.Ordinal)) + _result.LFSDiff.New.Oid = content.Substring(11); + else if (content.StartsWith(LFS_SIZE_PREFIX, StringComparison.Ordinal)) + _result.LFSDiff.New.Size = long.Parse(content.AsSpan(5)); } - else if (ch != '\\') + else if (prefix == PREFIX_CONTEXT) { - ProcessInlineHighlights(); - var match = REG_INDICATOR().Match(line); - if (match.Success) - { - _oldLine = int.Parse(match.Groups[1].Value); - _newLine = int.Parse(match.Groups[2].Value); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); - _result.TextDiff.Lines.Add(_last); - } - else - { - if (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal)) - { - _result.IsLFS = true; - _result.LFSDiff = new Models.LFSDiff(); - return; - } - - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine); - _result.TextDiff.Lines.Add(_last); - _oldLine++; - _newLine++; - } + if (content.StartsWith(LFS_SIZE_PREFIX, StringComparison.Ordinal)) + _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(content.AsSpan(5)); } - else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal)) + return true; + } + + if ((_oldLine == 1 && _newLine == 1 && prefix == PREFIX_CONTEXT) || + (_oldLine == 1 && _newLine == 0 && prefix == PREFIX_DELETED) || + (_oldLine == 0 && _newLine == 1 && prefix == PREFIX_ADDED)) + { + if (content.StartsWith(LFS_SPECIFIER, StringComparison.Ordinal)) { - _last.NoNewLineEndOfFile = true; + _isLFS = true; + _result.LFSDiff = new Models.LFSDiff(); + return true; } } + + return false; } private void ProcessInlineHighlights() @@ -277,5 +365,7 @@ private void ProcessInlineHighlights() private Models.TextDiffLine _last = null; private int _oldLine = 0; private int _newLine = 0; + private bool _isInChunk = false; + private bool _isLFS = false; } } diff --git a/src/Commands/DiffTool.cs b/src/Commands/DiffTool.cs index 5bc23db36..a102bd474 100644 --- a/src/Commands/DiffTool.cs +++ b/src/Commands/DiffTool.cs @@ -17,7 +17,7 @@ public void Open() var tool = Native.OS.GetDiffMergeTool(true); if (tool == null) { - App.RaiseException(Context, "Invalid diff/merge tool in preference setting!"); + RaiseException("Invalid diff/merge tool in preference setting!"); return; } @@ -40,7 +40,7 @@ public void Open() } catch (Exception ex) { - App.RaiseException(Context, ex.Message); + RaiseException(ex.Message); } } @@ -56,7 +56,7 @@ private bool CheckGitConfiguration() if (config.TryGetValue("merge.tool", out var mergeTool)) return CheckCLIBasedTool(mergeTool); - App.RaiseException(Context, "Missing git configuration: diff.guitool"); + RaiseException("Missing git configuration: diff.guitool"); return false; } @@ -65,7 +65,7 @@ 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!"); + RaiseException($"CLI based diff tool \"{tool}\" is not supported by this app!"); return false; } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index cbf38bd49..8d08b90a7 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -10,7 +10,7 @@ public static class Discard /// /// Discard all local changes (unstaged & staged) /// - public static async Task AllAsync(string repo, bool includeUntracked, bool includeIgnored, Models.ICommandLog log) + public static async Task AllAsync(string repo, bool includeModified, bool includeUntracked, bool includeIgnored, Models.ICommandLog log) { if (includeUntracked) { @@ -33,11 +33,11 @@ public static async Task AllAsync(string repo, bool includeUntracked, bool inclu } catch (Exception e) { - App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + Models.Notification.Send(repo, $"Failed to discard changes. Reason: {e.Message}", true); } if (includeIgnored) - await new Clean(repo, Models.CleanMode.All).Use(log).ExecAsync().ConfigureAwait(false); + await new Clean(repo, Models.CleanMode.UntrackedAndIgnoredFiles).Use(log).ExecAsync().ConfigureAwait(false); else await new Clean(repo, Models.CleanMode.OnlyUntrackedFiles).Use(log).ExecAsync().ConfigureAwait(false); } @@ -46,7 +46,8 @@ public static async Task AllAsync(string repo, bool includeUntracked, bool inclu await new Clean(repo, Models.CleanMode.OnlyIgnoredFiles).Use(log).ExecAsync().ConfigureAwait(false); } - await new Reset(repo, "HEAD", "--hard").Use(log).ExecAsync().ConfigureAwait(false); + if (includeModified) + await new Reset(repo, "", "--hard").Use(log).ExecAsync().ConfigureAwait(false); } /// @@ -79,14 +80,14 @@ public static async Task ChangesAsync(string repo, List changes, } catch (Exception e) { - App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + Models.Notification.Send(repo, $"Failed to discard changes. Reason: {e.Message}", true); } if (restores.Count > 0) { 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 9a599f821..914361257 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -22,6 +22,17 @@ public Fetch(string repo, string remote, bool noTags, bool force) Args = builder.ToString(); } + 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) { _remote = remote.Remote; diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs deleted file mode 100644 index bbefa34e5..000000000 --- a/src/Commands/GenerateCommitMessage.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - /// - /// A C# version of https://github.com/anjerodev/commitollama - /// - public class GenerateCommitMessage - { - public class GetDiffContent : Command - { - public GetDiffContent(string repo, Models.DiffOption opt) - { - WorkingDirectory = repo; - Context = repo; - Args = $"diff --no-color --no-ext-diff --diff-algorithm=minimal {opt}"; - } - - public async Task ReadAsync() - { - return await ReadToEndAsync().ConfigureAwait(false); - } - } - - public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onResponse) - { - _service = service; - _repo = repo; - _changes = changes; - _cancelToken = cancelToken; - _onResponse = onResponse; - } - - public async Task ExecAsync() - { - try - { - _onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n"); - - var responseBuilder = new StringBuilder(); - var summaryBuilder = new StringBuilder(); - foreach (var change in _changes) - { - if (_cancelToken.IsCancellationRequested) - return; - - responseBuilder.Append("- "); - summaryBuilder.Append("- "); - - var rs = await new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadAsync(); - if (rs.IsSuccess) - { - await _service.ChatAsync( - _service.AnalyzeDiffPrompt, - $"Here is the `git diff` output: {rs.StdOut}", - _cancelToken, - update => - { - responseBuilder.Append(update); - summaryBuilder.Append(update); - - _onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}"); - }); - } - - responseBuilder.AppendLine(); - summaryBuilder.Append("(file: ").Append(change.Path).AppendLine(")"); - } - - if (_cancelToken.IsCancellationRequested) - return; - - var responseBody = responseBuilder.ToString(); - var subjectBuilder = new StringBuilder(); - await _service.ChatAsync( - _service.GenerateSubjectPrompt, - $"Here are the summaries changes:\n{summaryBuilder}", - _cancelToken, - update => - { - subjectBuilder.Append(update); - _onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}"); - }); - } - catch (Exception e) - { - App.RaiseException(_repo, $"Failed to generate commit message: {e}"); - } - } - - private Models.OpenAIService _service; - private string _repo; - private List _changes; - private CancellationToken _cancelToken; - private Action _onResponse; - } -} diff --git a/src/Commands/GetFileChangeForAI.cs b/src/Commands/GetFileChangeForAI.cs new file mode 100644 index 000000000..128d0aba6 --- /dev/null +++ b/src/Commands/GetFileChangeForAI.cs @@ -0,0 +1,31 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class GetFileChangeForAI : Command + { + public GetFileChangeForAI(string repo, string file, string originalFile, string amendParent) + { + WorkingDirectory = repo; + Context = repo; + + var builder = new StringBuilder(); + builder.Append("diff --no-color --no-ext-diff --diff-algorithm=minimal --cached "); + if (!string.IsNullOrEmpty(amendParent)) + builder.Append(amendParent).Append(' '); + builder.Append("-- "); + if (!string.IsNullOrEmpty(originalFile) && !file.Equals(originalFile, StringComparison.Ordinal)) + builder.Append(originalFile.Quoted()).Append(' '); + builder.Append(file.Quoted()); + + Args = builder.ToString(); + } + + public async Task ReadAsync() + { + return await ReadToEndAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 56002e223..0438d76de 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -3,53 +3,80 @@ namespace SourceGit.Commands { - public static class GitFlow + public class GitFlow : Command { - public static async Task InitAsync(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log) + public GitFlow(string repo) { - var config = new Config(repo); - await config.SetAsync("gitflow.branch.master", master).ConfigureAwait(false); + WorkingDirectory = repo; + Context = repo; + } + + public async Task InitAsync(string production, string develop, string feature, string release, string hotfix, string tag) + { + if (Native.OS.GitFlowVersion == Models.GitFlowVersion.Next) + { + var builder = new StringBuilder(); + builder + .Append("flow init --preset=classic ") + .Append("--main=").Append(production.Quoted()).Append(' ') + .Append("--develop=").Append(develop.Quoted()).Append(' ') + .Append("--feature=").Append(feature).Append(' ') + .Append("--bugfix=bugfix/ ") + .Append("--release=").Append(release).Append(' ') + .Append("--hotfix=").Append(hotfix).Append(' ') + .Append("--support=support/"); + + if (!string.IsNullOrEmpty(tag)) + builder.Append(" --tag=").Append(tag); + + Args = builder.ToString(); + return await ExecAsync().ConfigureAwait(false); + } + + var config = new Config(WorkingDirectory); + await config.SetAsync("gitflow.branch.master", production).ConfigureAwait(false); await config.SetAsync("gitflow.branch.develop", develop).ConfigureAwait(false); await config.SetAsync("gitflow.prefix.feature", feature).ConfigureAwait(false); await config.SetAsync("gitflow.prefix.bugfix", "bugfix/").ConfigureAwait(false); await config.SetAsync("gitflow.prefix.release", release).ConfigureAwait(false); await config.SetAsync("gitflow.prefix.hotfix", hotfix).ConfigureAwait(false); await config.SetAsync("gitflow.prefix.support", "support/").ConfigureAwait(false); - await config.SetAsync("gitflow.prefix.versiontag", version, true).ConfigureAwait(false); + await config.SetAsync("gitflow.prefix.versiontag", tag, true).ConfigureAwait(false); - var init = new Command(); - init.WorkingDirectory = repo; - init.Context = repo; - init.Args = "flow init -d"; - return await init.Use(log).ExecAsync().ConfigureAwait(false); + Args = "flow init -d"; + return await ExecAsync().ConfigureAwait(false); } - public static async Task StartAsync(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) + public async Task StartAsync(Models.GitFlowBranchType type, string name, Models.Branch based) { - var start = new Command(); - start.WorkingDirectory = repo; - start.Context = repo; + var builder = new StringBuilder(); + builder.Append("flow "); switch (type) { case Models.GitFlowBranchType.Feature: - start.Args = $"flow feature start {name}"; + builder.Append("feature"); break; case Models.GitFlowBranchType.Release: - start.Args = $"flow release start {name}"; + builder.Append("release"); break; case Models.GitFlowBranchType.Hotfix: - start.Args = $"flow hotfix start {name}"; + builder.Append("hotfix"); break; default: - App.RaiseException(repo, "Bad git-flow branch type!!!"); + RaiseException("Bad git-flow branch type!!!"); return false; } - return await start.Use(log).ExecAsync().ConfigureAwait(false); + builder.Append(" start ").Append(name); + if (based != null) + builder.Append(' ').Append(based.Name); + + Args = builder.ToString(); + return await ExecAsync().ConfigureAwait(false); } - public static async Task FinishAsync(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) + public async Task FinishAsync(Models.GitFlowBranchType type, string name, bool rebase, bool squash, bool keepBranch) { var builder = new StringBuilder(); builder.Append("flow "); @@ -66,24 +93,21 @@ public static async Task FinishAsync(string repo, Models.GitFlowBranchType builder.Append("hotfix"); break; default: - App.RaiseException(repo, "Bad git-flow branch type!!!"); + RaiseException("Bad git-flow branch type!!!"); return false; } builder.Append(" finish "); + if (rebase) + builder.Append("--rebase "); if (squash) builder.Append("--squash "); - if (push) - builder.Append("--push "); if (keepBranch) - builder.Append("-k "); + builder.Append("--keep "); builder.Append(name); - var finish = new Command(); - finish.WorkingDirectory = repo; - finish.Context = repo; - finish.Args = builder.ToString(); - return await finish.Use(log).ExecAsync().ConfigureAwait(false); + Args = builder.ToString(); + return await ExecAsync().ConfigureAwait(false); } } } diff --git a/src/Commands/InteractiveRebase.cs b/src/Commands/InteractiveRebase.cs index 7e4ca86b2..6b61b9d2a 100644 --- a/src/Commands/InteractiveRebase.cs +++ b/src/Commands/InteractiveRebase.cs @@ -4,16 +4,18 @@ namespace SourceGit.Commands { public class InteractiveRebase : Command { - public InteractiveRebase(string repo, string basedOn, bool autoStash) + public InteractiveRebase(string repo, string basedOn, bool autoStash, bool noVerify) { WorkingDirectory = repo; Context = repo; Editor = EditorType.RebaseEditor; var builder = new StringBuilder(512); - builder.Append("rebase -i --autosquash "); + builder.Append("-c core.commentChar=\"^\" -c core.commentString=\"±\" rebase -i --autosquash "); if (autoStash) builder.Append("--autostash "); + if (noVerify) + builder.Append("--no-verify "); Args = builder.Append(basedOn).ToString(); } diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs index 087e71c7b..9dbe05459 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -8,11 +8,11 @@ public partial class IsBinary : Command [GeneratedRegex(@"^\-\s+\-\s+.*$")] private static partial Regex REG_TEST(); - public IsBinary(string repo, string commit, string path) + public IsBinary(string repo, string revision, string path) { WorkingDirectory = repo; Context = repo; - Args = $"diff --no-color --no-ext-diff --numstat {Models.Commit.EmptyTreeSHA1} {commit} -- {path.Quoted()}"; + Args = $"diff --no-color --no-ext-diff --numstat {Models.EmptyTreeHash.Guess(revision)} {revision} -- {path.Quoted()}"; RaiseError = false; } diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs deleted file mode 100644 index e5b752d35..000000000 --- a/src/Commands/IsConflictResolved.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - public class IsConflictResolved : Command - { - public IsConflictResolved(string repo, Models.Change change) - { - var opt = new Models.DiffOption(change, true); - - WorkingDirectory = repo; - Context = repo; - Args = $"diff --no-color --no-ext-diff -a --ignore-cr-at-eol --check {opt}"; - } - - public bool GetResult() - { - return ReadToEnd().IsSuccess; - } - - public async Task GetResultAsync() - { - var rs = await ReadToEndAsync().ConfigureAwait(false); - return rs.IsSuccess; - } - } -} diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs index bd11b5779..bcb8811f9 100644 --- a/src/Commands/Merge.cs +++ b/src/Commands/Merge.cs @@ -9,7 +9,6 @@ public Merge(string repo, string source, string mode, bool edit) { WorkingDirectory = repo; Context = repo; - Editor = EditorType.CoreEditor; var builder = new StringBuilder(); builder.Append("merge --progress "); diff --git a/src/Commands/MergeBase.cs b/src/Commands/MergeBase.cs new file mode 100644 index 000000000..2e349a84b --- /dev/null +++ b/src/Commands/MergeBase.cs @@ -0,0 +1,32 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public partial class MergeBase : Command + { + [GeneratedRegex(@"^[0-9a-f]{8,64}$")] + private static partial Regex REG_HEX(); + + public MergeBase(string repo, string rev1, string rev2) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = $"merge-base {rev1} {rev2}"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return string.Empty; + + var trimmed = rs.StdOut.Trim(); + if (REG_HEX().IsMatch(trimmed)) + return trimmed; + + return string.Empty; + } + } +} diff --git a/src/Commands/MergeTool.cs b/src/Commands/MergeTool.cs index 0f15fb617..45e17cc6e 100644 --- a/src/Commands/MergeTool.cs +++ b/src/Commands/MergeTool.cs @@ -17,7 +17,7 @@ public async Task OpenAsync() var tool = Native.OS.GetDiffMergeTool(false); if (tool == null) { - App.RaiseException(Context, "Invalid diff/merge tool in preference setting!"); + RaiseException("Invalid diff/merge tool in preference setting!"); return false; } @@ -46,14 +46,14 @@ private async Task CheckGitConfigurationAsync() if (string.IsNullOrEmpty(tool)) { - App.RaiseException(Context, "Missing git configuration: merge.guitool"); + RaiseException("Missing git configuration: merge.guitool"); return 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!"); + RaiseException($"CLI based merge tool \"{tool}\" is not supported by this app!"); return false; } diff --git a/src/Commands/MergeTree.cs b/src/Commands/MergeTree.cs new file mode 100644 index 000000000..41e9ea28a --- /dev/null +++ b/src/Commands/MergeTree.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class MergeTree : Command + { + public MergeTree(string repo, string source, string dest) + { + WorkingDirectory = repo; + Args = $"merge-tree --write-tree {source} {dest}"; + } + + public async Task GetExitCodeAsync() + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(false); + + var exitCode = -1; + try + { + proc.Start(); + await proc.WaitForExitAsync().ConfigureAwait(false); + exitCode = proc.ExitCode; + } + catch + { + // Ignore any exceptions and just return -1 + } + + return exitCode; + } + } +} diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index e121445ba..75b433e73 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -13,10 +13,13 @@ public Pull(string repo, string remote, string branch, bool useRebase) Context = repo; var builder = new StringBuilder(512); - builder.Append("pull --verbose --progress "); - if (useRebase) - builder.Append("--rebase=true "); - builder.Append(remote).Append(' ').Append(branch); + builder + .Append("pull --verbose --progress --rebase=") + .Append(useRebase ? "true" : "false") + .Append(' ') + .Append(remote) + .Append(' ') + .Append(branch); Args = builder.ToString(); } diff --git a/src/Commands/QueryAuthors.cs b/src/Commands/QueryAuthors.cs new file mode 100644 index 000000000..d5e443d5e --- /dev/null +++ b/src/Commands/QueryAuthors.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QueryAuthors : Command + { + public QueryAuthors(string repo) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = "log -100000 --all --format=%aN±%aE"; + } + + public async Task> GetResultAsync() + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return []; + + var start = 0; + var end = rs.StdOut.IndexOf('\n', start); + var added = new HashSet(); + var users = new List(); + while (end > 0) + { + var line = rs.StdOut.Substring(start, end - start); + if (!string.IsNullOrEmpty(line) && !added.Contains(line)) + { + var user = Models.User.FindOrAdd(line); + users.Add(user); + added.Add(line); + } + + start = end + 1; + if (start >= rs.StdOut.Length - 1) + break; + + end = rs.StdOut.IndexOf('\n', start); + } + + return users; + } + } +} diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs deleted file mode 100644 index 6af0abb73..000000000 --- a/src/Commands/QueryCommitChildren.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - public class QueryCommitChildren : Command - { - public QueryCommitChildren(string repo, string commit, int max) - { - WorkingDirectory = repo; - Context = repo; - _commit = commit; - Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}"; - } - - public async Task> GetResultAsync() - { - var rs = await ReadToEndAsync().ConfigureAwait(false); - var outs = new List(); - if (rs.IsSuccess) - { - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - if (line.Contains(_commit)) - outs.Add(line.Substring(0, 40)); - } - } - - return outs; - } - - private string _commit; - } -} diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 0e46ead81..1a5073988 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -28,10 +28,6 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method { builder.Append("-i --author=").Append(filter.Quoted()); } - else if (method == Models.CommitSearchMethod.ByCommitter) - { - builder.Append("-i --committer=").Append(filter.Quoted()); - } else if (method == Models.CommitSearchMethod.ByMessage) { var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries); @@ -80,7 +76,8 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method commit.Subject = parts[7]; commits.Add(commit); - findHead |= commit.IsMerged; + if (!findHead && commit.IsMerged) + findHead = true; } await proc.WaitForExitAsync().ConfigureAwait(false); @@ -103,7 +100,7 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method } catch (Exception e) { - App.RaiseException(Context, $"Failed to query commits. Reason: {e.Message}"); + RaiseException($"Failed to query commits. Reason: {e.Message}"); } return commits; diff --git a/src/Commands/QueryCommitsForInteractiveRebase.cs b/src/Commands/QueryCommitsForInteractiveRebase.cs index e1f964881..7f0d1ba1a 100644 --- a/src/Commands/QueryCommitsForInteractiveRebase.cs +++ b/src/Commands/QueryCommitsForInteractiveRebase.cs @@ -12,7 +12,7 @@ public QueryCommitsForInteractiveRebase(string repo, string on) WorkingDirectory = repo; Context = repo; - 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"; + 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%s%n%B%n{_boundary}\" {on}...HEAD"; } public async Task> GetResultAsync() @@ -21,7 +21,7 @@ public QueryCommitsForInteractiveRebase(string repo, string on) var rs = await ReadToEndAsync().ConfigureAwait(false); if (!rs.IsSuccess) { - App.RaiseException(Context, $"Failed to query commits for interactive-rebase. Reason: {rs.StdErr}"); + RaiseException($"Failed to query commits for interactive-rebase. Reason: {rs.StdErr}"); return commits; } @@ -58,6 +58,9 @@ public QueryCommitsForInteractiveRebase(string repo, string on) case 6: current.Commit.CommitterTime = ulong.Parse(line); break; + case 7: + current.Commit.Subject = line; + break; default: var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal); if (boundary > end) diff --git a/src/Commands/QueryConflictFileState.cs b/src/Commands/QueryConflictFileState.cs new file mode 100644 index 000000000..f9a52ee12 --- /dev/null +++ b/src/Commands/QueryConflictFileState.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QueryConflictFileState : Command + { + public QueryConflictFileState(string repo, Models.Change change) + { + var opt = new Models.DiffOption(change, true); + + WorkingDirectory = repo; + Context = repo; + Args = $"diff --no-color --no-ext-diff --no-textconv --full-index --patch {opt}"; + } + + public async Task GetResultAsync() + { + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + var isBinary = false; + var tokenCount = 0; + + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) + { + if (isBinary) + continue; + + if (line.StartsWith("Binary files ", StringComparison.Ordinal)) + isBinary = true; + else if (line.StartsWith("++<<<<<<<", StringComparison.Ordinal) || + line.StartsWith("++=======", StringComparison.Ordinal) || + line.StartsWith("++>>>>>>>", StringComparison.Ordinal)) + tokenCount++; + } + + await proc.WaitForExitAsync().ConfigureAwait(false); + + if (isBinary) + return Models.ConflictFileState.UnmergedBinary; + + return tokenCount == 0 ? Models.ConflictFileState.Resolved : Models.ConflictFileState.UnmergedText; + } + catch + { + return Models.ConflictFileState.Unknown; + } + } + } +} diff --git a/src/Commands/QueryCurrentBranchCommitHashes.cs b/src/Commands/QueryCurrentBranchCommitHashes.cs index a795e1f54..393160602 100644 --- a/src/Commands/QueryCurrentBranchCommitHashes.cs +++ b/src/Commands/QueryCurrentBranchCommitHashes.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -9,10 +8,9 @@ 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"; + Args = $"log --since=@{sinceTimestamp} --format=%H"; } public async Task> GetResultAsync() diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs index 66f964600..d111b5f21 100644 --- a/src/Commands/QueryFileContent.cs +++ b/src/Commands/QueryFileContent.cs @@ -27,7 +27,7 @@ public static async Task RunAsync(string repo, string revision, string f } catch (Exception e) { - App.RaiseException(repo, $"Failed to query file content: {e}"); + Models.Notification.Send(repo, $"Failed to query file content: {e}", true); } stream.Position = 0; @@ -58,7 +58,7 @@ public static async Task FromLFSAsync(string repo, string oid, long size } catch (Exception e) { - App.RaiseException(repo, $"Failed to query file content: {e}"); + Models.Notification.Send(repo, $"Failed to query file content: {e}", true); } stream.Position = 0; diff --git a/src/Commands/QueryFileHistory.cs b/src/Commands/QueryFileHistory.cs new file mode 100644 index 000000000..5dca109a2 --- /dev/null +++ b/src/Commands/QueryFileHistory.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public partial class QueryFileHistory : Command + { + [GeneratedRegex(@"^([MADT])\s+(.+)$")] + private static partial Regex REG_FORMAT(); + [GeneratedRegex(@"^([CR])[0-9]{0,4}\s+(.+)$")] + private static partial Regex REG_RENAME_FORMAT(); + + public QueryFileHistory(string repo, string path, string head) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + + var builder = new StringBuilder(); + builder.Append("log --no-show-signature --date-order -n 10000 --decorate=no --format=\"@%H%x00%P%x00%aN±%aE%x00%at%x00%s\" --follow --name-status "); + if (!string.IsNullOrEmpty(head)) + builder.Append(head).Append(" "); + builder.Append("-- ").Append(path.Quoted()); + + Args = builder.ToString(); + } + + public async Task> GetResultAsync() + { + var versions = new List(); + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return versions; + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + if (lines.Length == 0) + return versions; + + Models.FileVersion last = null; + foreach (var line in lines) + { + if (line.StartsWith('@')) + { + var parts = line.Split('\0'); + if (parts.Length != 5) + continue; + + last = new Models.FileVersion(); + last.SHA = parts[0].Substring(1); + last.HasParent = !string.IsNullOrEmpty(parts[1]); + last.Author = Models.User.FindOrAdd(parts[2]); + last.AuthorTime = ulong.Parse(parts[3]); + last.Subject = parts[4]; + versions.Add(last); + } + else if (last != null) + { + var match = REG_FORMAT().Match(line); + if (!match.Success) + { + match = REG_RENAME_FORMAT().Match(line); + if (match.Success) + { + var type = match.Groups[1].Value; + last.Change.Path = match.Groups[2].Value; + last.Change.Set(type == "R" ? Models.ChangeState.Renamed : Models.ChangeState.Copied); + } + + continue; + } + + last.Change.Path = match.Groups[2].Value; + + var status = match.Groups[1].Value; + switch (status[0]) + { + case 'M': + last.Change.Set(Models.ChangeState.Modified); + break; + case 'A': + last.Change.Set(Models.ChangeState.Added); + break; + case 'D': + last.Change.Set(Models.ChangeState.Deleted); + break; + case 'T': + last.Change.Set(Models.ChangeState.TypeChanged); + break; + } + } + } + + return versions; + } + } +} diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 385a95f27..d1bd30a2c 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -9,13 +10,21 @@ public partial class QueryLocalChanges : Command { [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")] private static partial Regex REG_FORMAT(); - private static readonly string[] UNTRACKED = ["no", "all"]; - public QueryLocalChanges(string repo, bool includeUntracked = true) + public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptionalLocks = true) { WorkingDirectory = repo; Context = repo; - Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; + + var builder = new StringBuilder(); + if (noOptionalLocks) + builder.Append("--no-optional-locks "); + if (includeUntracked) + builder.Append("-c core.untrackedCache=true -c status.showUntrackedFiles=all status -uall --ignore-submodules=dirty --porcelain"); + else + builder.Append("status -uno --ignore-submodules=dirty --porcelain"); + + Args = builder.ToString(); } public async Task> GetResultAsync() diff --git a/src/Commands/QueryPickableCommits.cs b/src/Commands/QueryPickableCommits.cs new file mode 100644 index 000000000..7ea26e5b2 --- /dev/null +++ b/src/Commands/QueryPickableCommits.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QueryPickableCommits : Command + { + public QueryPickableCommits(string repo, string based, string target) + { + WorkingDirectory = repo; + Context = repo; + Args = $"log --topo-order --cherry-pick --right-only --no-merges --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s {based}...{target}"; + } + + public async Task> GetResultAsync() + { + var commits = new List(); + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) + { + 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); + } + + await proc.WaitForExitAsync().ConfigureAwait(false); + } + catch (Exception e) + { + RaiseException($"Failed to query commits. Reason: {e.Message}"); + } + + return commits; + } + } +} diff --git a/src/Commands/QueryRemotes.cs b/src/Commands/QueryRemotes.cs index bd42aabf1..f8a23b7aa 100644 --- a/src/Commands/QueryRemotes.cs +++ b/src/Commands/QueryRemotes.cs @@ -24,6 +24,19 @@ public QueryRemotes(string repo) if (!rs.IsSuccess) return outs; + var config = await new Config(WorkingDirectory).ReadAllAsync().ConfigureAwait(false); + var disableAutoFetchRemotes = new HashSet(); + foreach (var (k, v) in config) + { + if (k.StartsWith("remote.", StringComparison.Ordinal) && + k.EndsWith(".disableautofetch", StringComparison.Ordinal) && + v.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + var remoteName = k.Substring(7, k.Length - 24).Trim('"'); + disableAutoFetchRemotes.Add(remoteName); + } + } + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { @@ -35,11 +48,22 @@ public QueryRemotes(string repo) { Name = match.Groups[1].Value, URL = match.Groups[2].Value, + DisableAutoFetch = disableAutoFetchRemotes.Contains(match.Groups[1].Value) }; 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/QueryRepositoryStatus.cs b/src/Commands/QueryRepositoryStatus.cs index 32c2489a6..3d6adac8e 100644 --- a/src/Commands/QueryRepositoryStatus.cs +++ b/src/Commands/QueryRepositoryStatus.cs @@ -6,11 +6,8 @@ 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(); + [GeneratedRegex(@"\+(\d+) \-(\d+)")] + private static partial Regex REG_BRANCH_AB(); public QueryRepositoryStatus(string repo) { @@ -20,23 +17,27 @@ public QueryRepositoryStatus(string repo) public async Task GetResultAsync() { - Args = "branch -l -v --format=\"%(refname:short)%00%(HEAD)%00%(upstream:track,nobracket)\""; + Args = "status --porcelain=v2 -b"; 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; + var count = lines.Length; + if (count < 2) + return null; - status.CurrentBranch = parts[0]; - if (!string.IsNullOrEmpty(parts[2])) - ParseTrackStatus(status, parts[2]); - } + var sha1 = lines[0].Substring(13).Trim(); // Remove "# branch.oid " prefix + var head = lines[1].Substring(14).Trim(); // Remove "# branch.head " prefix + + if (head.Equals("(detached)", StringComparison.Ordinal)) + status.CurrentBranch = sha1.Length > 10 ? $"({sha1.Substring(0, 10)})" : "-"; + else + status.CurrentBranch = head; + + if (count == 4 && lines[3].StartsWith("# branch.ab ", StringComparison.Ordinal)) + ParseTrackStatus(status, lines[3].Substring(12).Trim()); status.LocalChanges = await new CountLocalChanges(WorkingDirectory, true) { RaiseError = false } .GetResultAsync() @@ -47,13 +48,12 @@ public QueryRepositoryStatus(string repo) 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); + var match = REG_BRANCH_AB().Match(input); + if (match.Success) + { + status.Ahead = int.Parse(match.Groups[1].Value); + status.Behind = int.Parse(match.Groups[2].Value); + } } } } diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs index 229d9e65e..0cd420592 100644 --- a/src/Commands/QueryStagedChangesWithAmend.cs +++ b/src/Commands/QueryStagedChangesWithAmend.cs @@ -6,25 +6,34 @@ namespace SourceGit.Commands { public partial class QueryStagedChangesWithAmend : Command { - [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")] + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{4,64}) [0-9a-f]{4,64} ([ADMT])\d{0,6}\t(.*)$")] private static partial Regex REG_FORMAT1(); - [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")] + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{4,64}) [0-9a-f]{4,64} ([RC])\d{0,6}\t(.*\t.*)$")] private static partial Regex REG_FORMAT2(); - public QueryStagedChangesWithAmend(string repo, string parent) + public QueryStagedChangesWithAmend(string repo) { WorkingDirectory = repo; Context = repo; - Args = $"diff-index --cached -M {parent}"; - _parent = parent; } public List GetResult() { + Args = "show --no-show-signature --format=\"%H %P\" -s HEAD"; var rs = ReadToEnd(); if (!rs.IsSuccess) return []; + var shas = rs.StdOut.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (shas.Length == 0) + return []; + + var parent = shas.Length > 1 ? shas[1] : Models.EmptyTreeHash.Guess(shas[0]); + Args = $"diff-index --cached -M {parent}"; + rs = ReadToEnd(); + if (!rs.IsSuccess) + return []; + var changes = new List(); var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) @@ -34,15 +43,16 @@ public QueryStagedChangesWithAmend(string repo, string parent) { var change = new Models.Change() { - Path = match.Groups[3].Value, + Path = match.Groups[4].Value, DataForAmend = new Models.ChangeDataForAmend() { FileMode = match.Groups[1].Value, ObjectHash = match.Groups[2].Value, - ParentSHA = _parent, + ParentSHA = parent, }, }; - change.Set(Models.ChangeState.Renamed); + var type = match.Groups[3].Value; + change.Set(type == "R" ? Models.ChangeState.Renamed : Models.ChangeState.Copied); changes.Add(change); continue; } @@ -57,7 +67,7 @@ public QueryStagedChangesWithAmend(string repo, string parent) { FileMode = match.Groups[1].Value, ObjectHash = match.Groups[2].Value, - ParentSHA = _parent, + ParentSHA = parent, }, }; @@ -67,9 +77,6 @@ public QueryStagedChangesWithAmend(string repo, string parent) case "A": change.Set(Models.ChangeState.Added); break; - case "C": - change.Set(Models.ChangeState.Copied); - break; case "D": change.Set(Models.ChangeState.Deleted); break; @@ -86,7 +93,5 @@ public QueryStagedChangesWithAmend(string repo, string parent) return changes; } - - private readonly string _parent; } } diff --git a/src/Commands/QueryStagedFileBlobGuid.cs b/src/Commands/QueryStagedFileBlobGuid.cs deleted file mode 100644 index 54ecd2ac7..000000000 --- a/src/Commands/QueryStagedFileBlobGuid.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - public partial class QueryStagedFileBlobGuid : Command - { - [GeneratedRegex(@"^\d+\s+([0-9a-f]+)\s+.*$")] - private static partial Regex REG_FORMAT(); - - public QueryStagedFileBlobGuid(string repo, string file) - { - WorkingDirectory = repo; - Context = repo; - Args = $"ls-files -s -- {file.Quoted()}"; - } - - public async Task GetResultAsync() - { - var rs = await ReadToEndAsync().ConfigureAwait(false); - var match = REG_FORMAT().Match(rs.StdOut.Trim()); - return match.Success ? match.Groups[1].Value : string.Empty; - } - } -} diff --git a/src/Commands/QuerySubmoduleRevision.cs b/src/Commands/QuerySubmoduleRevision.cs new file mode 100644 index 000000000..a70f1efb6 --- /dev/null +++ b/src/Commands/QuerySubmoduleRevision.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QuerySubmoduleRevision : Command + { + public QuerySubmoduleRevision(string repo, string revision) + { + WorkingDirectory = repo; + Context = repo; + + if (revision.EndsWith("-dirty", StringComparison.Ordinal)) + { + _hasUncommittedChange = true; + _revision = revision.Substring(0, revision.Length - 6); + } + else + { + _hasUncommittedChange = false; + _revision = revision; + } + } + + public async Task GetResultAsync() + { + Args = $"show --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s%x00%B -s {_revision}"; + + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess || string.IsNullOrEmpty(rs.StdOut)) + return null; + + var commit = new Models.Commit(); + var lines = rs.StdOut.Split('\0'); + if (lines.Length < 9) + 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 message = new Models.CommitFullMessage() { Message = lines[8].TrimEnd() }; + var uncommittedChangesCount = 0; + if (_hasUncommittedChange) + uncommittedChangesCount = await new CountLocalChanges(WorkingDirectory, true) + .GetResultAsync() + .ConfigureAwait(false); + + return new Models.RevisionSubmodule() + { + Commit = commit, + FullMessage = message, + UncommittedChanges = uncommittedChangesCount + }; + } + + private bool _hasUncommittedChange = false; + private string _revision = string.Empty; + } +} diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 8718542eb..9033a8d04 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%(taggername)±%(taggeremail)%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:trim)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; } public async Task> GetResultAsync() 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 f7f33ac5c..fdc585c01 100644 --- a/src/Commands/Rebase.cs +++ b/src/Commands/Rebase.cs @@ -4,15 +4,17 @@ namespace SourceGit.Commands { public class Rebase : Command { - public Rebase(string repo, string basedOn, bool autoStash) + public Rebase(string repo, string basedOn, bool autoStash, bool noVerify) { WorkingDirectory = repo; Context = repo; var builder = new StringBuilder(512); - builder.Append("rebase "); + builder.Append("-c core.commentChar=\"^\" -c core.commentString=\"±\" rebase "); if (autoStash) builder.Append("--autostash "); + if (noVerify) + builder.Append("--no-verify "); Args = builder.Append(basedOn).ToString(); } diff --git a/src/Commands/Remove.cs b/src/Commands/Remove.cs new file mode 100644 index 000000000..eb715e2ca --- /dev/null +++ b/src/Commands/Remove.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands +{ + public class Remove : Command + { + public Remove(string repo, List files) + { + WorkingDirectory = repo; + Context = repo; + + var builder = new StringBuilder(); + builder.Append("rm -f --"); + foreach (var file in files) + builder.Append(' ').Append(file.Quoted()); + + Args = builder.ToString(); + } + } +} diff --git a/src/Commands/Replay.cs b/src/Commands/Replay.cs new file mode 100644 index 000000000..3f2546224 --- /dev/null +++ b/src/Commands/Replay.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class Replay : Command + { + public Replay(string repo, string onto, string range) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = $"replay --onto {onto} {range}"; + } + + public async Task GetExitCodeAsync() + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(false); + + var exitCode = -1; + try + { + proc.Start(); + await proc.WaitForExitAsync().ConfigureAwait(false); + exitCode = proc.ExitCode; + } + catch + { + // Ignore any exceptions and just return -1 + } + + return exitCode; + } + } +} diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs index 6a54533b1..f640d0de4 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 --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/SaveChangesAsPatch.cs b/src/Commands/SaveChangesAsPatch.cs index 51bc1319b..4ec83a6a6 100644 --- a/src/Commands/SaveChangesAsPatch.cs +++ b/src/Commands/SaveChangesAsPatch.cs @@ -69,7 +69,7 @@ private static async Task ProcessSingleChangeAsync(string repo, Models.Dif } catch (Exception e) { - App.RaiseException(repo, "Save change to patch failed: " + e.Message); + Models.Notification.Send(repo, "Save change to patch failed: " + e.Message, true); return false; } } diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index e05d6d358..410d15b07 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -55,7 +55,7 @@ private static async Task ExecCmdAsync(string repo, string args, string outputFi } catch (Exception e) { - App.RaiseException(repo, "Save file failed: " + e.Message); + Models.Notification.Send(repo, "Save file failed: " + e.Message, true); } } } diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs index 3bf72308e..31b5b71cc 100644 --- a/src/Commands/Stash.cs +++ b/src/Commands/Stash.cs @@ -76,6 +76,12 @@ public async Task ApplyAsync(string name, bool restoreIndex) return await ExecAsync().ConfigureAwait(false); } + public async Task CheckoutBranchAsync(string name, string branch) + { + Args = $"stash branch {branch.Quoted()} {name.Quoted()}"; + return await ExecAsync().ConfigureAwait(false); + } + public async Task PopAsync(string name) { Args = $"stash pop -q --index {name.Quoted()}"; diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 2d43c7629..3c6ed017b 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -1,15 +1,28 @@ using System.IO; +using System.Text; using System.Threading.Tasks; namespace SourceGit.Commands { public class Statistics : Command { - public Statistics(string repo, int max) + public Statistics(string repo, int max, Models.Branch specBranch) { WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE"; + + var builder = new StringBuilder(); + builder + .Append("log --date-order -") + .Append(max) + .Append(" --format=%ct$%aN±%aE "); + + if (specBranch == null || string.IsNullOrEmpty(specBranch.FullName)) + builder.Append("--branches --remotes"); + else + builder.Append(specBranch.FullName); + + Args = builder.ToString(); } public async Task ReadAsync() diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs index bb8d884ac..d8b1dbcb7 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -43,7 +43,7 @@ public async Task SetBranchAsync(string path, string branch) return await ExecAsync().ConfigureAwait(false); } - public async Task UpdateAsync(List modules, bool init, bool recursive, bool useRemote = false) + public async Task UpdateAsync(List modules, bool init, bool recursive, bool useRemote) { var builder = new StringBuilder(); builder.Append("submodule update"); diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UpdateIndexInfo.cs similarity index 86% rename from src/Commands/UnstageChangesForAmend.cs rename to src/Commands/UpdateIndexInfo.cs index 9170306a0..d8c47ca91 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,6 +60,8 @@ public async Task ExecAsync() starter.RedirectStandardInput = true; starter.RedirectStandardOutput = false; starter.RedirectStandardError = true; + starter.StandardInputEncoding = new UTF8Encoding(false); + starter.StandardErrorEncoding = Encoding.UTF8; try { @@ -72,13 +74,13 @@ public async Task ExecAsync() var rs = proc.ExitCode == 0; if (!rs) - App.RaiseException(_repo, err); + Models.Notification.Send(_repo, err, true); return rs; } catch (Exception e) { - App.RaiseException(_repo, "Failed to unstage changes: " + e.Message); + Models.Notification.Send(_repo, "Failed to update index: " + e.Message, true); return false; } } diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index 50ee97242..7b70e2ab4 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Text; using System.Threading.Tasks; @@ -29,7 +28,6 @@ public Worktree(string repo) if (line.StartsWith("worktree ", StringComparison.Ordinal)) { last = new Models.Worktree() { FullPath = line.Substring(9).Trim() }; - last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath); worktrees.Add(last); continue; } @@ -39,8 +37,7 @@ public Worktree(string repo) if (line.StartsWith("bare", StringComparison.Ordinal)) { - worktrees.Remove(last); - last = null; + last.IsBare = true; } else if (line.StartsWith("HEAD ", StringComparison.Ordinal)) { diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs index 8a2f31416..5c5dd9047 100644 --- a/src/Converters/BoolConverters.cs +++ b/src/Converters/BoolConverters.cs @@ -1,4 +1,6 @@ -using Avalonia.Data.Converters; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; using Avalonia.Media; namespace SourceGit.Converters @@ -6,9 +8,12 @@ namespace SourceGit.Converters public static class BoolConverters { public static readonly FuncValueConverter IsBoldToFontWeight = - new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Regular); + new(x => x ? FontWeight.Bold : FontWeight.Regular); public static readonly FuncValueConverter IsMergedToOpacity = - new FuncValueConverter(x => x ? 1 : 0.65); + new(x => x ? 1 : 0.65); + + public static readonly FuncValueConverter IsWarningToBrush = + new(x => x ? Brushes.DarkGoldenrod : Application.Current?.FindResource("Brush.FG1") as IBrush); } } diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 7d2dabe25..f44fa96d5 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -1,37 +1,26 @@ using Avalonia; -using Avalonia.Controls; using Avalonia.Data.Converters; -using Avalonia.Media; namespace SourceGit.Converters { public static class IntConverters { public static readonly FuncValueConverter IsGreaterThanZero = - new FuncValueConverter(v => v > 0); + new(v => v > 0); public static readonly FuncValueConverter IsGreaterThanFour = - new FuncValueConverter(v => v > 4); + new(v => v > 4); public static readonly FuncValueConverter IsZero = - new FuncValueConverter(v => v == 0); - - public static readonly FuncValueConverter IsOne = - new FuncValueConverter(v => v == 1); + new(v => v == 0); public static readonly FuncValueConverter IsNotOne = - new FuncValueConverter(v => v != 1); - - public static readonly FuncValueConverter IsSubjectLengthBad = - new FuncValueConverter(v => v > ViewModels.Preferences.Instance.SubjectGuideLength); - - public static readonly FuncValueConverter IsSubjectLengthGood = - new FuncValueConverter(v => v <= ViewModels.Preferences.Instance.SubjectGuideLength); + new(v => v != 1); public static readonly FuncValueConverter ToTreeMargin = - new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); + new(v => new Thickness(v * 16, 0, 0, 0)); - public static readonly FuncValueConverter ToBookmarkBrush = - new FuncValueConverter(v => Models.Bookmarks.Get(v) ?? App.Current?.FindResource("Brush.FG1") as IBrush); + public static readonly FuncValueConverter ToUnsolvedDesc = + new(v => v == 0 ? App.Text("MergeConflictEditor.AllResolved") : App.Text("MergeConflictEditor.ConflictsRemaining", v)); } } diff --git a/src/Converters/InteractiveRebaseActionConverters.cs b/src/Converters/InteractiveRebaseActionConverters.cs index 76967ce3e..81f5564a7 100644 --- a/src/Converters/InteractiveRebaseActionConverters.cs +++ b/src/Converters/InteractiveRebaseActionConverters.cs @@ -6,7 +6,7 @@ namespace SourceGit.Converters public static class InteractiveRebaseActionConverters { public static readonly FuncValueConverter ToIconBrush = - new FuncValueConverter(v => + new(v => { return v switch { @@ -20,6 +20,12 @@ public static class InteractiveRebaseActionConverters }); public static readonly FuncValueConverter ToName = - new FuncValueConverter(v => v.ToString()); + new(v => v.ToString()); + + public static readonly FuncValueConverter IsDrop = + new(v => v == Models.InteractiveRebaseAction.Drop); + + public static readonly FuncValueConverter ToOpacity = + new(v => v > Models.InteractiveRebaseAction.Reword ? 0.65 : 1.0); } } diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index 7d2821914..4a6fd33d3 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -2,6 +2,7 @@ using System.Globalization; using Avalonia.Data.Converters; +using Avalonia.Input; using Avalonia.Styling; namespace SourceGit.Converters @@ -87,5 +88,8 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu public static readonly FuncValueConverter ToFriendlyUpstream = new FuncValueConverter(v => v is { Length: > 13 } ? v.Substring(13) : string.Empty); + + public static readonly FuncValueConverter FromKeyGesture = + new FuncValueConverter(v => v?.ToString("p", null) ?? string.Empty); } } diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index cf4eb3e38..9616ac64d 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -53,6 +53,9 @@ public void Start() Task.Run(async () => { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(2); + while (true) { string email = null; @@ -74,16 +77,20 @@ public void Start() var md5 = GetEmailHash(email); var matchGitHubUser = REG_GITHUB_USER_EMAIL().Match(email); - var url = matchGitHubUser.Success ? - $"https://avatars.githubusercontent.com/{matchGitHubUser.Groups[2].Value}" : - $"https://www.gravatar.com/avatar/{md5}?d=404"; + var url = $"https://www.gravatar.com/avatar/{md5}?d=404"; + if (matchGitHubUser.Success) + { + var githubUser = matchGitHubUser.Groups[2].Value; + if (githubUser.EndsWith("[bot]", StringComparison.OrdinalIgnoreCase)) + githubUser = githubUser.Substring(0, githubUser.Length - 5); + + url = $"https://avatars.githubusercontent.com/{githubUser}"; + } var localFile = Path.Combine(_storePath, md5); Bitmap img = null; try { - using var client = new HttpClient(); - client.Timeout = TimeSpan.FromSeconds(2); var rsp = await client.GetAsync(url); if (rsp.IsSuccessStatusCode) { @@ -218,10 +225,7 @@ private string GetEmailHash(string email) { var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim(); var hash = MD5.HashData(Encoding.Default.GetBytes(lowered)); - var builder = new StringBuilder(hash.Length * 2); - foreach (var c in hash) - builder.Append(c.ToString("x2")); - return builder.ToString(); + return Convert.ToHexStringLower(hash); } private void NotifyResourceChanged(string email, Bitmap image) diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs index 2ed8beb25..286a02c4f 100644 --- a/src/Models/Bisect.cs +++ b/src/Models/Bisect.cs @@ -6,16 +6,19 @@ namespace SourceGit.Models public enum BisectState { None = 0, - WaitingForRange, - Detecting, + WaitingForFirstBad, + WaitingForCheckoutAnother, + WaitingForFirstGood, + WaitingForMark, } [Flags] public enum BisectCommitFlag { None = 0, - Good = 1 << 0, - Bad = 1 << 1, + Good, + Bad, + Skipped, } public class Bisect @@ -31,5 +34,11 @@ public HashSet Goods get; set; } = []; + + public HashSet Skipped + { + get; + set; + } = []; } } diff --git a/src/Models/Blame.cs b/src/Models/Blame.cs index 3eb8d8bf3..a8fac34c8 100644 --- a/src/Models/Blame.cs +++ b/src/Models/Blame.cs @@ -6,15 +6,15 @@ public class BlameLineInfo { public bool IsFirstInGroup { get; set; } = false; public string CommitSHA { get; set; } = string.Empty; + public string File { get; set; } = string.Empty; public string Author { get; set; } = string.Empty; - public string Time { get; set; } = string.Empty; + public ulong Timestamp { get; set; } = 0; } public class BlameData { - public string File { get; set; } = string.Empty; - public List LineInfos { get; set; } = new List(); - public string Content { get; set; } = string.Empty; public bool IsBinary { get; set; } = false; + public string Content { get; set; } = string.Empty; + public List LineInfos { get; set; } = []; } } diff --git a/src/Models/Change.cs b/src/Models/Change.cs index 2ad448add..baf6e8500 100644 --- a/src/Models/Change.cs +++ b/src/Models/Change.cs @@ -60,7 +60,7 @@ public void Set(ChangeState index, ChangeState workTree = ChangeState.None) Index = index; WorkTree = workTree; - if (index == ChangeState.Renamed || workTree == ChangeState.Renamed) + if (index == ChangeState.Renamed || index == ChangeState.Copied || workTree == ChangeState.Renamed) { var parts = Path.Split('\t', 2); if (parts.Length < 2) diff --git a/src/Models/CleanMode.cs b/src/Models/CleanMode.cs index fde113878..0c7d7858d 100644 --- a/src/Models/CleanMode.cs +++ b/src/Models/CleanMode.cs @@ -4,6 +4,6 @@ public enum CleanMode { OnlyUntrackedFiles = 0, OnlyIgnoredFiles, - All, + UntrackedAndIgnoredFiles, } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 4a98b985a..de299ed6b 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { @@ -7,16 +8,13 @@ public enum CommitSearchMethod { BySHA = 0, ByAuthor, - ByCommitter, ByMessage, ByPath, ByContent, } - public class Commit + public class Commit : ObservableObject { - public const string EmptyTreeSHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; - public string SHA { get; set; } = string.Empty; public User Author { get; set; } = User.Invalid; public ulong AuthorTime { get; set; } = 0; @@ -30,14 +28,16 @@ public class Commit 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 IsHighlightedInGraph + { + get => _isHighlightedInGraph; + set => SetProperty(ref _isHighlightedInGraph, value); + } public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; public bool HasDecorators => Decorators.Count > 0; + public string FirstParentToCompare => Parents.Count > 0 ? $"{SHA}^" : EmptyTreeHash.Guess(SHA); public string GetFriendlyName() { @@ -124,6 +124,8 @@ public void ParseDecorators(string data) return NumericSort.Compare(l.Name, r.Name); }); } + + private bool _isHighlightedInGraph = false; } public class CommitFullMessage diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 82505c351..8557f271d 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -8,6 +8,14 @@ namespace SourceGit.Models { public record CommitGraphLayout(double StartY, double ClipWidth, double RowHeight); + public enum CommitGraphHighlighting + { + All = 0, + CurrentBranchOnly, + SelectedCommitsOnly, + CurrentBranchAndSelectedCommits, + } + public class CommitGraph { public static List Pens { get; } = []; @@ -27,11 +35,11 @@ public static void SetPens(List colors, double thickness) s_penCount = colors.Count; } - public class Path(int color, bool isMerged) + public class Path(int color, bool isHighlighted) { public List Points { get; } = []; public int Color { get; } = color; - public bool IsMerged { get; } = isMerged; + public bool IsHighlighted { get; } = isHighlighted; } public class Link @@ -40,7 +48,7 @@ public class Link public Point Control; public Point End; public int Color; - public bool IsMerged; + public bool IsHighlighted; } public enum DotType @@ -55,14 +63,14 @@ public class Dot public DotType Type; public Point Center; public int Color; - public bool IsMerged; + public bool IsHighlighted; } public List Paths { get; } = []; public List Links { get; } = []; public List Dots { get; } = []; - public static CommitGraph Parse(List commits, bool firstParentOnlyEnabled) + public static CommitGraph Generate(List commits, bool recalculateMergeState, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet highlightExtraCommits) { const double unitWidth = 12; const double halfWidth = 6; @@ -74,11 +82,28 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable var ended = new List(); var offsetY = -halfHeight; var colorPicker = new ColorPicker(); + var merged = new HashSet(); foreach (var commit in commits) { PathHelper major = null; - var isMerged = commit.IsMerged; + + // Update merge state of this commit. + if (recalculateMergeState) + { + if (commit.IsMerged) + { + merged.Remove(commit.SHA); + foreach (var p in commit.Parents) + merged.Add(p); + } + else if (merged.Remove(commit.SHA)) + { + commit.IsMerged = true; + foreach (var p in commit.Parents) + merged.Add(p); + } + } // Update current y offset offsetY += unitHeight; @@ -86,6 +111,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable // Find first curves that links to this commit and marks others that links to this commit ended. var offsetX = 4 - halfWidth; var maxOffsetOld = unsolved.Count > 0 ? unsolved[^1].LastX : offsetX + unitWidth; + var isHighlighted = false; foreach (var l in unsolved) { if (l.Next.Equals(commit.SHA, StringComparison.Ordinal)) @@ -94,6 +120,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable { offsetX += unitWidth; major = l; + isHighlighted = major.IsHighlighted; if (commit.Parents.Count > 0) { @@ -110,9 +137,10 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable { l.End(major.LastX, offsetY, halfHeight); ended.Add(l); - } - isMerged = isMerged || l.IsMerged; + if (!isHighlighted && l.IsHighlighted) + isHighlighted = true; + } } else { @@ -129,6 +157,42 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable } ended.Clear(); + // Calculate highlighted state + if (!isHighlighted) + { + if (highlighting == CommitGraphHighlighting.All) + { + isHighlighted = true; + } + else if (highlighting == CommitGraphHighlighting.CurrentBranchOnly) + { + isHighlighted = commit.IsMerged; + } + else if (highlighting == CommitGraphHighlighting.SelectedCommitsOnly) + { + isHighlighted = highlightExtraCommits.Remove(commit.SHA); + if (isHighlighted) + { + foreach (var p in commit.Parents) + highlightExtraCommits.Add(p); + } + } + else + { + if (commit.IsMerged) + { + isHighlighted = true; + } + else if (highlightExtraCommits.Remove(commit.SHA)) + { + isHighlighted = true; + foreach (var p in commit.Parents) + highlightExtraCommits.Add(p); + } + } + } + commit.IsHighlightedInGraph = isHighlighted; + // If no path found, create new curve for branch head // Otherwise, create new curve for new merged commit if (major == null) @@ -137,21 +201,21 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable if (commit.Parents.Count > 0) { - major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY)); + major = new PathHelper(commit.Parents[0], isHighlighted, colorPicker.Next(), new Point(offsetX, offsetY)); unsolved.Add(major); temp.Paths.Add(major.Path); } } - else if (isMerged && !major.IsMerged && commit.Parents.Count > 0) + else if (isHighlighted && !major.IsHighlighted && commit.Parents.Count > 0) { - major.ReplaceMerged(); + major.Highlight(); temp.Paths.Add(major.Path); } // Calculate link position of this commit. var position = new Point(major?.LastX ?? offsetX, offsetY); var dotColor = major?.Path.Color ?? 0; - var anchor = new Dot() { Center = position, Color = dotColor, IsMerged = isMerged }; + var anchor = new Dot() { Center = position, Color = dotColor, IsHighlighted = isHighlighted }; if (commit.IsCurrentHead) anchor.Type = DotType.Head; else if (commit.Parents.Count > 1) @@ -169,10 +233,10 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal)); if (parent != null) { - if (isMerged && !parent.IsMerged) + if (isHighlighted && !parent.IsHighlighted) { parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight); - parent.ReplaceMerged(); + parent.Highlight(); temp.Paths.Add(parent.Path); } @@ -182,7 +246,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable End = new Point(parent.LastX, offsetY + halfHeight), Control = new Point(parent.LastX, position.Y), Color = parent.Path.Color, - IsMerged = isMerged, + IsHighlighted = isHighlighted, }); } else @@ -190,15 +254,14 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable offsetX += unitWidth; // Create new curve for parent commit that not includes before - var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight)); + var l = new PathHelper(parentHash, isHighlighted, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight)); unsolved.Add(l); temp.Paths.Add(l.Path); } } } - // Margins & merge state (used by Views.Histories). - commit.IsMerged = isMerged; + // Margins & colors (used by Views.Histories). commit.Color = dotColor; commit.LeftMargin = Math.Max(offsetX, maxOffsetOld) + halfWidth + 2; } @@ -246,26 +309,25 @@ private class PathHelper public Path Path { get; private set; } public string Next { get; set; } public double LastX { get; private set; } + public bool IsHighlighted { get => Path.IsHighlighted; } - public bool IsMerged => Path.IsMerged; - - public PathHelper(string next, bool isMerged, int color, Point start) + public PathHelper(string next, bool IsHighlighted, int color, Point start) { Next = next; LastX = start.X; _lastY = start.Y; - Path = new Path(color, isMerged); + Path = new Path(color, IsHighlighted); Path.Points.Add(start); } - public PathHelper(string next, bool isMerged, int color, Point start, Point to) + public PathHelper(string next, bool IsHighlighted, int color, Point start, Point to) { Next = next; LastX = to.X; _lastY = to.Y; - Path = new Path(color, isMerged); + Path = new Path(color, IsHighlighted); Path.Points.Add(start); Path.Points.Add(to); } @@ -346,9 +408,9 @@ public void End(double x, double y, double halfHeight) } /// - /// End the current path and create a new from the end. + /// End the current path and create a new highlighted from the end. /// - public void ReplaceMerged() + public void Highlight() { var color = Path.Color; Add(LastX, _lastY); diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs index e7c3f697a..fa78f206c 100644 --- a/src/Models/CommitLink.cs +++ b/src/Models/CommitLink.cs @@ -40,6 +40,8 @@ public static List Get(List remotes) 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/ConfirmButtonType.cs b/src/Models/ConfirmButtonType.cs new file mode 100644 index 000000000..e0d50b8f1 --- /dev/null +++ b/src/Models/ConfirmButtonType.cs @@ -0,0 +1,8 @@ +namespace SourceGit.Models +{ + public enum ConfirmButtonType + { + OkCancel = 0, + YesNo, + } +} diff --git a/src/Models/ConfirmEmptyCommitResult.cs b/src/Models/ConfirmEmptyCommitResult.cs index 176845b9a..9c36493e3 100644 --- a/src/Models/ConfirmEmptyCommitResult.cs +++ b/src/Models/ConfirmEmptyCommitResult.cs @@ -3,6 +3,7 @@ public enum ConfirmEmptyCommitResult { Cancel = 0, + StageSelectedAndCommit, StageAllAndCommit, CreateEmptyCommit, } diff --git a/src/Models/Conflict.cs b/src/Models/Conflict.cs new file mode 100644 index 000000000..fd9c69030 --- /dev/null +++ b/src/Models/Conflict.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public enum ConflictFileState + { + Unknown, + UnmergedText, + UnmergedBinary, + Resolved + } + + public enum ConflictPanelType + { + Ours, + Theirs, + Result + } + + public enum ConflictResolution + { + None, + UseOurs, + UseTheirs, + UseBothMineFirst, + UseBothTheirsFirst, + } + + public enum ConflictLineType + { + None, + Common, + Marker, + Ours, + Theirs, + } + + public enum ConflictLineState + { + Normal, + ConflictBlockStart, + ConflictBlock, + ConflictBlockEnd, + ResolvedBlockStart, + ResolvedBlock, + ResolvedBlockEnd, + } + + public class ConflictLine + { + public ConflictLineType Type { get; set; } = ConflictLineType.None; + public string Content { get; set; } = string.Empty; + public string LineNumber { get; set; } = string.Empty; + + public ConflictLine() + { + } + public ConflictLine(ConflictLineType type, string content) + { + Type = type; + Content = content; + } + public ConflictLine(ConflictLineType type, string content, int lineNumber) + { + Type = type; + Content = content; + LineNumber = lineNumber.ToString(); + } + } + + public record ConflictSelectedChunk( + double Y, + double Height, + int ConflictIndex, + ConflictPanelType Panel, + bool IsResolved + ); + + public class ConflictRegion + { + public int StartLineInOriginal { get; set; } + public int EndLineInOriginal { get; set; } + + public string StartMarker { get; set; } = "<<<<<<<"; + public string SeparatorMarker { get; set; } = "======="; + public string EndMarker { get; set; } = ">>>>>>>"; + + public List OursContent { get; set; } = new(); + public List TheirsContent { get; set; } = new(); + + public bool IsResolved { get; set; } = false; + public ConflictResolution ResolutionType { get; set; } = ConflictResolution.None; + } +} diff --git a/src/Models/ConventionalCommitType.cs b/src/Models/ConventionalCommitType.cs index 9275ff116..bf2763a41 100644 --- a/src/Models/ConventionalCommitType.cs +++ b/src/Models/ConventionalCommitType.cs @@ -6,9 +6,10 @@ namespace SourceGit.Models { public class ConventionalCommitType { - public string Name { get; set; } - public string Type { get; set; } - public string Description { get; set; } + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string PrefillShortDesc { get; set; } = string.Empty; public ConventionalCommitType(string name, string type, string description) { diff --git a/src/Models/Count.cs b/src/Models/Count.cs index d48b0c083..2be46c8f9 100644 --- a/src/Models/Count.cs +++ b/src/Models/Count.cs @@ -1,19 +1,4 @@ -using System; - -namespace SourceGit.Models +namespace SourceGit.Models { - public class Count : IDisposable - { - public int Value { get; set; } = 0; - - public Count(int value) - { - Value = value; - } - - public void Dispose() - { - // Ignore - } - } + public record Count(int Value); } diff --git a/src/Models/CustomAction.cs b/src/Models/CustomAction.cs index ec500d3c0..59652d32f 100644 --- a/src/Models/CustomAction.cs +++ b/src/Models/CustomAction.cs @@ -19,6 +19,8 @@ public enum CustomActionControlType PathSelector, CheckBox, ComboBox, + LocalBranchSelector, + RemoteBranchSelector, } public record CustomActionTargetFile(string File, Commit Revision); @@ -49,6 +51,12 @@ public string StringValue set => SetProperty(ref _stringValue, value); } + public string StringFormatter + { + get => _stringFormatter; + set => SetProperty(ref _stringFormatter, value); + } + public bool BoolValue { get => _boolValue; @@ -59,6 +67,7 @@ public bool BoolValue private string _label = string.Empty; private string _description = string.Empty; private string _stringValue = string.Empty; + private string _stringFormatter = string.Empty; private bool _boolValue = false; } diff --git a/src/Models/DateTimeFormat.cs b/src/Models/DateTimeFormat.cs index 16276c40c..d313d56ae 100644 --- a/src/Models/DateTimeFormat.cs +++ b/src/Models/DateTimeFormat.cs @@ -1,50 +1,77 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace SourceGit.Models { public class DateTimeFormat { - public string DateOnly { get; set; } - public string DateTime { get; set; } + public static readonly List Supported = new List + { + new("yyyy/MM/dd"), + new("yyyy.MM.dd"), + new("yyyy-MM-dd"), + new("MM/dd/yyyy"), + new("MM.dd.yyyy"), + new("MM-dd-yyyy"), + new("dd/MM/yyyy"), + new("dd.MM.yyyy"), + new("dd-MM-yyyy"), + new("MMM d yyyy"), + new("d MMM yyyy"), + }; + + public static int ActiveIndex + { + get; + set; + } = 0; + + public static bool Use24Hours + { + get; + set; + } = true; + + public string DateFormat + { + get; + } public string Example { - get => _example.ToString(DateTime); + get => DateTime.Now.ToString(DateFormat, _culture); } - public DateTimeFormat(string dateOnly, string dateTime) + private static readonly CultureInfo _culture = CreateCulture(); + + private static CultureInfo CreateCulture() { - DateOnly = dateOnly; - DateTime = dateTime; + var culture = (CultureInfo)CultureInfo.CurrentCulture.Clone(); + culture.DateTimeFormat.DateSeparator = "/"; + culture.DateTimeFormat.TimeSeparator = ":"; + return culture; } - public static int ActiveIndex + public DateTimeFormat(string date) { - get; - set; - } = 0; + DateFormat = date; + } - public static DateTimeFormat Active + public static string Format(ulong timestamp, bool dateOnly = false) { - get => Supported[ActiveIndex]; + var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); + return Format(localTime, dateOnly); } - public static readonly List Supported = new List + public static string Format(DateTime localTime, bool dateOnly = false) { - new DateTimeFormat("yyyy/MM/dd", "yyyy/MM/dd, HH:mm:ss"), - new DateTimeFormat("yyyy.MM.dd", "yyyy.MM.dd, HH:mm:ss"), - new DateTimeFormat("yyyy-MM-dd", "yyyy-MM-dd, HH:mm:ss"), - new DateTimeFormat("MM/dd/yyyy", "MM/dd/yyyy, HH:mm:ss"), - new DateTimeFormat("MM.dd.yyyy", "MM.dd.yyyy, HH:mm:ss"), - new DateTimeFormat("MM-dd-yyyy", "MM-dd-yyyy, HH:mm:ss"), - new DateTimeFormat("dd/MM/yyyy", "dd/MM/yyyy, HH:mm:ss"), - new DateTimeFormat("dd.MM.yyyy", "dd.MM.yyyy, HH:mm:ss"), - new DateTimeFormat("dd-MM-yyyy", "dd-MM-yyyy, HH:mm:ss"), - new DateTimeFormat("MMM d yyyy", "MMM d yyyy, HH:mm:ss"), - new DateTimeFormat("d MMM yyyy", "d MMM yyyy, HH:mm:ss"), - }; + var actived = Supported[ActiveIndex]; + if (dateOnly) + return localTime.ToString(actived.DateFormat, _culture); - private static readonly DateTime _example = new DateTime(2025, 1, 31, 8, 0, 0, DateTimeKind.Local); + var format = Use24Hours ? $"{actived.DateFormat} HH:mm:ss" : $"{actived.DateFormat} hh:mm:ss tt"; + return localTime.ToString(format, _culture); + } } } diff --git a/src/Models/DealWithLocalChanges.cs b/src/Models/DealWithLocalChanges.cs new file mode 100644 index 000000000..9775c61a4 --- /dev/null +++ b/src/Models/DealWithLocalChanges.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models +{ + public enum DealWithLocalChanges + { + DoNothing = 0, + StashAndReapply, + Discard, + } +} diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index 2ecfe458f..af64b39a4 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -1,20 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; namespace SourceGit.Models { public class DiffOption { - /// - /// Enable `--ignore-cr-at-eol` by default? - /// - public static bool IgnoreCRAtEOL - { - get; - set; - } = true; - - public Change WorkingCopyChange => _workingCopyChange; + public bool IsLocalChange => _revisions.Count == 0; public bool IsUnstaged => _isUnstaged; public List Revisions => _revisions; public string Path => _path; @@ -27,7 +19,6 @@ public static bool IgnoreCRAtEOL /// public DiffOption(Change change, bool isUnstaged) { - _workingCopyChange = change; _isUnstaged = isUnstaged; _path = change.Path; _orgPath = change.OriginalPath; @@ -59,24 +50,67 @@ public DiffOption(Change change, bool isUnstaged) /// public DiffOption(Commit commit, Change change) { - var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; - _revisions.Add(baseRevision); + _revisions.Add(commit.FirstParentToCompare); _revisions.Add(commit.SHA); _path = change.Path; _orgPath = change.OriginalPath; } /// - /// Diff with filepath. Used by FileHistories + /// Used to diff in `FileHistory` /// - /// - /// - public DiffOption(Commit commit, string file) + /// + public DiffOption(FileVersion ver) { - var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; - _revisions.Add(baseRevision); - _revisions.Add(commit.SHA); - _path = file; + if (string.IsNullOrEmpty(ver.OriginalPath)) + { + _revisions.Add(ver.HasParent ? $"{ver.SHA}^" : EmptyTreeHash.Guess(ver.SHA)); + _revisions.Add(ver.SHA); + _path = ver.Path; + } + else + { + _revisions.Add($"{ver.SHA}^:{ver.OriginalPath.Quoted()}"); + _revisions.Add($"{ver.SHA}:{ver.Path.Quoted()}"); + _path = ver.Path; + _orgPath = ver.Change.OriginalPath; + _ignorePaths = true; + } + } + + /// + /// Used to diff two revisions in `FileHistory` + /// + /// + /// + public DiffOption(FileVersion start, FileVersion end) + { + if (start.Change.Index == ChangeState.Deleted) + { + _revisions.Add(EmptyTreeHash.Guess(end.SHA)); + _revisions.Add(end.SHA); + _path = end.Path; + } + else if (end.Change.Index == ChangeState.Deleted) + { + _revisions.Add(start.SHA); + _revisions.Add(EmptyTreeHash.Guess(start.SHA)); + _path = start.Path; + } + else if (!end.Path.Equals(start.Path, StringComparison.Ordinal)) + { + _revisions.Add($"{start.SHA}:{start.Path.Quoted()}"); + _revisions.Add($"{end.SHA}:{end.Path.Quoted()}"); + _path = end.Path; + _orgPath = start.Path; + _ignorePaths = true; + } + else + { + _revisions.Add(start.SHA); + _revisions.Add(end.SHA); + _path = start.Path; + } } /// @@ -104,6 +138,9 @@ public override string ToString() foreach (var r in _revisions) builder.Append($"{r} "); + if (_ignorePaths) + return builder.ToString(); + builder.Append("-- "); if (!string.IsNullOrEmpty(_orgPath)) builder.Append($"{_orgPath.Quoted()} "); @@ -112,11 +149,11 @@ public override string ToString() return builder.ToString(); } - private readonly Change _workingCopyChange = null; private readonly bool _isUnstaged = false; private readonly string _path; private readonly string _orgPath = string.Empty; private readonly string _extra = string.Empty; private readonly List _revisions = []; + private readonly bool _ignorePaths = false; } } diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 6c381df35..ddb3d7e07 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Text.RegularExpressions; using Avalonia.Media.Imaging; namespace SourceGit.Models @@ -23,6 +22,7 @@ public class TextRange(int p, int n) public class TextDiffLine { public TextDiffLineType Type { get; set; } = TextDiffLineType.None; + public byte[] RawContent { get; set; } = []; public string Content { get; set; } = ""; public int OldLineNumber { get; set; } = 0; public int NewLineNumber { get; set; } = 0; @@ -33,542 +33,26 @@ public class TextDiffLine public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); public TextDiffLine() { } - public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine) + public TextDiffLine(TextDiffLineType type, string content, byte[] rawContent, int oldLine, int newLine) { Type = type; Content = content; + RawContent = rawContent; OldLineNumber = oldLine; NewLineNumber = newLine; } } - public class TextDiffSelection - { - public int StartLine { get; set; } = 0; - public int EndLine { get; set; } = 0; - public bool HasChanges { get; set; } = false; - public int IgnoredAdds { get; set; } = 0; - public int IgnoredDeletes { get; set; } = 0; - } - public partial class TextDiff { public List Lines { get; set; } = new List(); public int MaxLineNumber = 0; - - public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide) - { - var rs = new TextDiffSelection(); - rs.StartLine = startLine; - rs.EndLine = endLine; - - for (int i = 0; i < startLine - 1; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Added) - rs.IgnoredAdds++; - else if (line.Type == TextDiffLineType.Deleted) - rs.IgnoredDeletes++; - } - - for (int i = startLine - 1; i < endLine; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Added) - { - if (isCombined || !isOldSide) - { - rs.HasChanges = true; - break; - } - } - else if (line.Type == TextDiffLineType.Deleted) - { - if (isCombined || isOldSide) - { - rs.HasChanges = true; - break; - } - } - } - - return rs; - } - - public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) - { - var isTracked = !string.IsNullOrEmpty(fileBlobGuid); - 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"); - writer.WriteLine($"index 00000000...{fileGuid}"); - writer.WriteLine($"--- {(revert || isTracked ? $"a/{change.Path}" : "/dev/null")}"); - writer.WriteLine($"+++ b/{change.Path}"); - - var additions = selection.EndLine - selection.StartLine; - if (selection.StartLine != 1) - additions++; - - if (revert) - { - var totalLines = Lines.Count - 1; - writer.WriteLine($"@@ -0,{totalLines - additions} +0,{totalLines} @@"); - for (int i = 1; i <= totalLines; i++) - { - var line = Lines[i]; - if (line.Type != TextDiffLineType.Added) - continue; - - if (i >= selection.StartLine - 1 && i < selection.EndLine) - writer.WriteLine($"+{line.Content}"); - else - writer.WriteLine($" {line.Content}"); - } - } - else - { - writer.WriteLine($"@@ -0,0 +0,{additions} @@"); - for (int i = selection.StartLine - 1; i < selection.EndLine; i++) - { - var line = Lines[i]; - if (line.Type != TextDiffLineType.Added) - continue; - writer.WriteLine($"+{line.Content}"); - } - } - - writer.WriteLine("\\ No newline at end of file"); - writer.Flush(); - } - - public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, string output) - { - 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}"); - writer.WriteLine($"+++ b/{change.Path}"); - - // If last line of selection is a change. Find one more line. - string tail = null; - if (selection.EndLine < Lines.Count) - { - var lastLine = Lines[selection.EndLine - 1]; - if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) - { - for (int i = selection.EndLine; i < Lines.Count; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - break; - if (line.Type == TextDiffLineType.Normal || - (revert && line.Type == TextDiffLineType.Added) || - (!revert && line.Type == TextDiffLineType.Deleted)) - { - tail = line.Content; - break; - } - } - } - } - - // If the first line is not indicator. - if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator) - { - var indicator = selection.StartLine - 1; - for (int i = selection.StartLine - 2; i >= 0; i--) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - { - indicator = i; - break; - } - } - - var ignoreAdds = 0; - var ignoreRemoves = 0; - for (int i = 0; i < indicator; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Added) - { - ignoreAdds++; - } - else if (line.Type == TextDiffLineType.Deleted) - { - ignoreRemoves++; - } - } - - for (int i = indicator; i < selection.StartLine - 1; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - { - ProcessIndicatorForPatch(writer, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, tail != null); - } - else if (line.Type == TextDiffLineType.Added) - { - if (revert) - WriteLine(writer, ' ', line); - } - else if (line.Type == TextDiffLineType.Deleted) - { - if (!revert) - WriteLine(writer, ' ', line); - } - else if (line.Type == TextDiffLineType.Normal) - { - WriteLine(writer, ' ', line); - } - } - } - - // Outputs the selected lines. - for (int i = selection.StartLine - 1; i < selection.EndLine; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - { - if (!ProcessIndicatorForPatch(writer, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, tail != null)) - break; - } - else if (line.Type == TextDiffLineType.Normal) - { - WriteLine(writer, ' ', line); - } - else if (line.Type == TextDiffLineType.Added) - { - WriteLine(writer, '+', line); - } - else if (line.Type == TextDiffLineType.Deleted) - { - WriteLine(writer, '-', line); - } - } - - writer.WriteLine($" {tail}"); - writer.Flush(); - } - - public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, bool isOldSide, string output) - { - 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}"); - writer.WriteLine($"+++ b/{change.Path}"); - - // If last line of selection is a change. Find one more line. - string tail = null; - if (selection.EndLine < Lines.Count) - { - var lastLine = Lines[selection.EndLine - 1]; - if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) - { - for (int i = selection.EndLine; i < Lines.Count; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - break; - if (revert) - { - if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) - { - tail = line.Content; - break; - } - } - else - { - if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) - { - tail = line.Content; - break; - } - } - } - } - } - - // If the first line is not indicator. - if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator) - { - var indicator = selection.StartLine - 1; - for (int i = selection.StartLine - 2; i >= 0; i--) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - { - indicator = i; - break; - } - } - - var ignoreAdds = 0; - var ignoreRemoves = 0; - for (int i = 0; i < indicator; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Added) - { - ignoreAdds++; - } - else if (line.Type == TextDiffLineType.Deleted) - { - ignoreRemoves++; - } - } - - for (int i = indicator; i < selection.StartLine - 1; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - { - ProcessIndicatorForPatchSingleSide(writer, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, isOldSide, tail != null); - } - else if (line.Type == TextDiffLineType.Added) - { - if (revert) - WriteLine(writer, ' ', line); - } - else if (line.Type == TextDiffLineType.Deleted) - { - if (!revert) - WriteLine(writer, ' ', line); - } - else if (line.Type == TextDiffLineType.Normal) - { - WriteLine(writer, ' ', line); - } - } - } - - // Outputs the selected lines. - for (int i = selection.StartLine - 1; i < selection.EndLine; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Indicator) - { - if (!ProcessIndicatorForPatchSingleSide(writer, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, isOldSide, tail != null)) - break; - } - else if (line.Type == TextDiffLineType.Normal) - { - WriteLine(writer, ' ', line); - } - else if (line.Type == TextDiffLineType.Added) - { - if (isOldSide) - { - if (revert) - { - WriteLine(writer, ' ', line); - } - else - { - selection.IgnoredAdds++; - } - } - else - { - WriteLine(writer, '+', line); - } - } - else if (line.Type == TextDiffLineType.Deleted) - { - if (isOldSide) - { - WriteLine(writer, '-', line); - } - else - { - if (!revert) - { - WriteLine(writer, ' ', line); - } - else - { - selection.IgnoredDeletes++; - } - } - } - } - - writer.WriteLine($" {tail}"); - writer.Flush(); - } - - private bool ProcessIndicatorForPatch(StreamWriter writer, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed) - { - var match = REG_INDICATOR().Match(indicator.Content); - var oldStart = int.Parse(match.Groups[1].Value); - var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; - var oldCount = 0; - var newCount = 0; - for (int i = idx + 1; i < end; i++) - { - var test = Lines[i]; - if (test.Type == TextDiffLineType.Indicator) - break; - - if (test.Type == TextDiffLineType.Normal) - { - oldCount++; - newCount++; - } - else if (test.Type == TextDiffLineType.Added) - { - if (i < start - 1) - { - if (revert) - { - newCount++; - oldCount++; - } - } - else - { - newCount++; - } - - if (i == end - 1 && tailed) - { - newCount++; - oldCount++; - } - } - else if (test.Type == TextDiffLineType.Deleted) - { - if (i < start - 1) - { - if (!revert) - { - newCount++; - oldCount++; - } - } - else - { - oldCount++; - } - - if (i == end - 1 && tailed) - { - newCount++; - oldCount++; - } - } - } - - if (oldCount == 0 && newCount == 0) - return false; - - writer.WriteLine($"@@ -{oldStart},{oldCount} +{newStart},{newCount} @@"); - return true; - } - - private bool ProcessIndicatorForPatchSingleSide(StreamWriter writer, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool isOldSide, bool tailed) - { - var match = REG_INDICATOR().Match(indicator.Content); - var oldStart = int.Parse(match.Groups[1].Value); - var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; - var oldCount = 0; - var newCount = 0; - for (int i = idx + 1; i < end; i++) - { - var test = Lines[i]; - if (test.Type == TextDiffLineType.Indicator) - break; - - if (test.Type == TextDiffLineType.Normal) - { - oldCount++; - newCount++; - } - else if (test.Type == TextDiffLineType.Added) - { - if (i < start - 1 || isOldSide) - { - if (revert) - { - newCount++; - oldCount++; - } - } - else - { - newCount++; - } - - if (i == end - 1 && tailed) - { - newCount++; - oldCount++; - } - } - else if (test.Type == TextDiffLineType.Deleted) - { - if (i < start - 1) - { - if (!revert) - { - newCount++; - oldCount++; - } - } - else - { - if (isOldSide) - { - oldCount++; - } - else - { - if (!revert) - { - newCount++; - oldCount++; - } - } - } - - if (i == end - 1 && tailed) - { - newCount++; - oldCount++; - } - } - } - - if (oldCount == 0 && newCount == 0) - return false; - - writer.WriteLine($"@@ -{oldStart},{oldCount} +{newStart},{newCount} @@"); - 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(); + public int AddedLines { get; set; } = 0; + public int DeletedLines { get; set; } = 0; + public int OldMode { get; set; } = 0; + public int NewMode { get; set; } = 0; + public string OldHash { get; set; } = string.Empty; + public string NewHash { get; set; } = string.Empty; } public class LFSDiff @@ -595,37 +79,34 @@ public class ImageDiff public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0"; } + public class EmptyFile + { + public const string SHA1 = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"; + public const string SHA256 = "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813"; + } + public class NoOrEOLChange; public class SubmoduleDiff { + public string FullPath { get; set; } = string.Empty; public RevisionSubmodule Old { get; set; } = null; public RevisionSubmodule New { get; set; } = null; + + public bool CanOpenDetails => File.Exists(Path.Combine(FullPath, ".git")) && + Old != null && Old.Commit.Author != User.Invalid && + New != null && New.Commit.Author != User.Invalid; } public class DiffResult { public bool IsBinary { get; set; } = false; - public bool IsLFS { get; set; } = false; + public bool IsSubmoduleChange { get; set; } = false; public string OldHash { get; set; } = string.Empty; public string NewHash { get; set; } = string.Empty; - public string OldMode { get; set; } = string.Empty; - public string NewMode { get; set; } = string.Empty; + public int OldMode { get; set; } = 0; + public int NewMode { get; set; } = 0; public TextDiff TextDiff { get; set; } = null; public LFSDiff LFSDiff { get; set; } = null; - - public string FileModeChange - { - get - { - if (string.IsNullOrEmpty(OldMode) && string.IsNullOrEmpty(NewMode)) - return string.Empty; - - var oldDisplay = string.IsNullOrEmpty(OldMode) ? "0" : OldMode; - var newDisplay = string.IsNullOrEmpty(NewMode) ? "0" : NewMode; - - return $"{oldDisplay} → {newDisplay}"; - } - } } } diff --git a/src/Models/EmptyTreeHash.cs b/src/Models/EmptyTreeHash.cs new file mode 100644 index 000000000..bf1445a0f --- /dev/null +++ b/src/Models/EmptyTreeHash.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Models +{ + public static class EmptyTreeHash + { + public static string Guess(string revision) + { + return revision.Length == 40 ? SHA1 : SHA256; + } + + private const string SHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; + private const string SHA256 = "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"; + } +} diff --git a/src/Models/ExternalMerger.cs b/src/Models/ExternalMerger.cs index ce90a7d44..655a1d58a 100644 --- a/src/Models/ExternalMerger.cs +++ b/src/Models/ExternalMerger.cs @@ -50,7 +50,7 @@ static ExternalMerger() { Supported = new List() { new ExternalMerger("git", "Use Git Settings", "", "", ""), - new ExternalMerger("xcode", "FileMerge", "/usr/bin/opendiff", "\"$BASE\" \"$LOCAL\" \"$REMOTE\" -ancestor \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + 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\""), diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index bd04f2f90..7f93cf8bd 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -12,15 +12,30 @@ namespace SourceGit.Models { public class ExternalTool { + public class LaunchOption + { + public string Title { get; set; } + public string Args { get; set; } + + public LaunchOption(string title, string args) + { + Title = title; + Args = args; + } + } + public string Name { get; } public string ExecFile { get; } public Bitmap IconImage { get; } + public bool SupportOpenFolder { get; } - public ExternalTool(string name, string icon, string execFile, Func execArgsGenerator = null) + public ExternalTool(string name, string icon, string execFile, Func> optionsGenerator, bool supportOpenFolder) { Name = name; ExecFile = execFile; - _execArgsGenerator = execArgsGenerator ?? (path => path.Quoted()); + SupportOpenFolder = supportOpenFolder; + + _optionsGenerator = optionsGenerator; try { @@ -34,17 +49,25 @@ public ExternalTool(string name, string icon, string execFile, Func MakeLaunchOptions(string repo) { - Process.Start(new ProcessStartInfo() + return _optionsGenerator?.Invoke(repo); + } + + public void Launch(string args) + { + if (File.Exists(ExecFile)) { - FileName = ExecFile, - Arguments = _execArgsGenerator.Invoke(path), - UseShellExecute = false, - }); + Process.Start(new ProcessStartInfo() + { + FileName = ExecFile, + Arguments = args, + UseShellExecute = false, + }); + } } - private Func _execArgsGenerator = null; + private Func> _optionsGenerator = null; } public class VisualStudioInstance @@ -61,40 +84,30 @@ public class VisualStudioInstance public class JetBrainsState { - [JsonPropertyName("version")] - public int Version { get; set; } = 0; - [JsonPropertyName("appVersion")] - public string AppVersion { get; set; } = string.Empty; [JsonPropertyName("tools")] public List Tools { get; set; } = new List(); } public class JetBrainsTool { - [JsonPropertyName("channelId")] - public string ChannelId { get; set; } - [JsonPropertyName("toolId")] - public string ToolId { get; set; } [JsonPropertyName("productCode")] public string ProductCode { get; set; } - [JsonPropertyName("tag")] - public string Tag { get; set; } [JsonPropertyName("displayName")] public string DisplayName { get; set; } [JsonPropertyName("displayVersion")] public string DisplayVersion { get; set; } - [JsonPropertyName("buildNumber")] - public string BuildNumber { get; set; } [JsonPropertyName("installLocation")] public string InstallLocation { get; set; } [JsonPropertyName("launchCommand")] public string LaunchCommand { get; set; } } - public class ExternalToolPaths + public class ExternalToolCustomization { [JsonPropertyName("tools")] public Dictionary Tools { get; set; } = new Dictionary(); + [JsonPropertyName("excludes")] + public List Excludes { get; set; } = new List(); } public class ExternalToolsFinder @@ -113,7 +126,7 @@ public ExternalToolsFinder() if (File.Exists(customPathsConfig)) { using var stream = File.OpenRead(customPathsConfig); - _customPaths = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.ExternalToolPaths); + _customization = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.ExternalToolCustomization); } } catch @@ -121,41 +134,39 @@ public ExternalToolsFinder() // Ignore } - _customPaths ??= new ExternalToolPaths(); + _customization ??= new ExternalToolCustomization(); } - public void TryAdd(string name, string icon, Func finder, Func execArgsGenerator = null) + public void TryAdd(string name, string icon, Func finder, Func> optionsGenerator = null, bool supportOpenFolder = true) { - if (_customPaths.Tools.TryGetValue(name, out var customPath) && File.Exists(customPath)) + if (_customization.Excludes.Contains(name)) + return; + + if (_customization.Tools.TryGetValue(name, out var customPath) && File.Exists(customPath)) { - Tools.Add(new ExternalTool(name, icon, customPath, execArgsGenerator)); + Tools.Add(new ExternalTool(name, icon, customPath, optionsGenerator, supportOpenFolder)); } else { var path = finder(); if (!string.IsNullOrEmpty(path) && File.Exists(path)) - Tools.Add(new ExternalTool(name, icon, path, execArgsGenerator)); + Tools.Add(new ExternalTool(name, icon, path, optionsGenerator, supportOpenFolder)); } } public void VSCode(Func platformFinder) { - TryAdd("Visual Studio Code", "vscode", platformFinder); + TryAdd("Visual Studio Code", "vscode", platformFinder, GenerateVSCodeLaunchOptions); } public void VSCodeInsiders(Func platformFinder) { - TryAdd("Visual Studio Code - Insiders", "vscode_insiders", platformFinder); + TryAdd("Visual Studio Code - Insiders", "vscode_insiders", platformFinder, GenerateVSCodeLaunchOptions); } public void VSCodium(Func platformFinder) { - TryAdd("VSCodium", "codium", platformFinder); - } - - public void Fleet(Func platformFinder) - { - TryAdd("Fleet", "fleet", platformFinder); + TryAdd("VSCodium", "codium", platformFinder, GenerateVSCodeLaunchOptions); } public void SublimeText(Func platformFinder) @@ -175,26 +186,53 @@ public void Cursor(Func platformFinder) public void FindJetBrainsFromToolbox(Func platformFinder) { - var exclude = new List { "fleet", "dotmemory", "dottrace", "resharper-u", "androidstudio" }; - var supportedIcons = new List { "CL", "DB", "DL", "DS", "GO", "JB", "PC", "PS", "PY", "QA", "QD", "RD", "RM", "RR", "WRS", "WS" }; + var supported = new List { "CL", "DB", "DL", "DS", "GO", "JB", "PC", "PS", "PY", "QA", "QD", "RD", "RM", "RR", "WRS", "WS" }; 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 (!supported.Contains(tool.ProductCode)) + continue; + + Tools.Add(new ExternalTool( + $"{tool.DisplayName} {tool.DisplayVersion}", + $"JetBrains/{tool.ProductCode}", + Path.Combine(tool.InstallLocation, tool.LaunchCommand), + null, + true)); + } + } + catch + { + // Ignore exceptions. } } } - private ExternalToolPaths _customPaths = null; + private List GenerateVSCodeLaunchOptions(string path) + { + var root = new DirectoryInfo(path); + if (!root.Exists) + return null; + + var options = new List(); + var prefixLen = root.FullName.Length; + root.WalkFiles(f => + { + if (f.EndsWith(".code-workspace", StringComparison.OrdinalIgnoreCase)) + { + var display = f.Substring(prefixLen).TrimStart(Path.DirectorySeparatorChar); + options.Add(new(display, f.Quoted())); + } + }, 2); + return options; + } + + private ExternalToolCustomization _customization = null; } } diff --git a/src/Models/FileVersion.cs b/src/Models/FileVersion.cs new file mode 100644 index 000000000..d822b9605 --- /dev/null +++ b/src/Models/FileVersion.cs @@ -0,0 +1,14 @@ +namespace SourceGit.Models +{ + public class FileVersion + { + public string SHA { get; set; } = string.Empty; + public bool HasParent { get; set; } = false; + public User Author { get; set; } = User.Invalid; + public ulong AuthorTime { get; set; } = 0; + public string Subject { get; set; } = string.Empty; + public Change Change { get; set; } = new(); + public string Path => Change.Path; + public string OriginalPath => Change.OriginalPath; + } +} diff --git a/src/Models/GitFlow.cs b/src/Models/GitFlow.cs index 05ade0ace..a648d4db2 100644 --- a/src/Models/GitFlow.cs +++ b/src/Models/GitFlow.cs @@ -1,5 +1,15 @@ -namespace SourceGit.Models +using System; +using System.Collections.Generic; + +namespace SourceGit.Models { + public enum GitFlowVersion + { + None = 0, + Legacy, + Next, + } + public enum GitFlowBranchType { None = 0, @@ -10,18 +20,74 @@ public enum GitFlowBranchType public class GitFlow { - public string Master { get; set; } = string.Empty; - public string Develop { get; set; } = string.Empty; + public string ProductionBranch { get; set; } = string.Empty; + public string DevelopmentBranch { get; set; } = string.Empty; public string FeaturePrefix { get; set; } = string.Empty; public string ReleasePrefix { get; set; } = string.Empty; public string HotfixPrefix { get; set; } = string.Empty; + public void Parse(Dictionary config) + { + // Reset to default values + ProductionBranch = string.Empty; + DevelopmentBranch = string.Empty; + FeaturePrefix = string.Empty; + ReleasePrefix = string.Empty; + HotfixPrefix = string.Empty; + + // Try to parse `git-flow-next` style configuration first if the `git-flow-next` is installed. + if (Native.OS.GitFlowVersion == GitFlowVersion.Next && + config.TryGetValue("gitflow.initialized", out var initialized) && + initialized.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + foreach (var kv in config) + { + if (!kv.Key.StartsWith("gitflow.branch.", StringComparison.Ordinal)) + continue; + + if (kv.Key.EndsWith(".type", StringComparison.Ordinal) && kv.Value.Equals("base", StringComparison.Ordinal)) + { + var b = kv.Key.Substring("gitflow.branch.".Length, kv.Key.Length - "gitflow.branch.".Length - ".type".Length); + if (config.ContainsKey($"gitflow.branch.{b}.parent")) + DevelopmentBranch = b; + else + ProductionBranch = b; + } + else if (kv.Key.EndsWith(".prefix", StringComparison.Ordinal)) + { + var t = kv.Key.Substring("gitflow.branch.".Length, kv.Key.Length - "gitflow.branch.".Length - ".prefix".Length); + if (t.Equals("feature", StringComparison.Ordinal)) + FeaturePrefix = kv.Value; + else if (t.Equals("release", StringComparison.Ordinal)) + ReleasePrefix = kv.Value; + else if (t.Equals("hotfix", StringComparison.Ordinal)) + HotfixPrefix = kv.Value; + } + } + } + + // Fall back to `git-flow` style configuration if `git-flow-next` style is not valid. + if (!IsValid) + { + if (config.TryGetValue("gitflow.branch.master", out var masterName)) + ProductionBranch = masterName; + if (config.TryGetValue("gitflow.branch.develop", out var developName)) + DevelopmentBranch = developName; + if (config.TryGetValue("gitflow.prefix.feature", out var featurePrefix)) + FeaturePrefix = featurePrefix; + if (config.TryGetValue("gitflow.prefix.release", out var releasePrefix)) + ReleasePrefix = releasePrefix; + if (config.TryGetValue("gitflow.prefix.hotfix", out var hotfixPrefix)) + HotfixPrefix = hotfixPrefix; + } + } + public bool IsValid { get { - return !string.IsNullOrEmpty(Master) && - !string.IsNullOrEmpty(Develop) && + return !string.IsNullOrEmpty(ProductionBranch) && + !string.IsNullOrEmpty(DevelopmentBranch) && !string.IsNullOrEmpty(FeaturePrefix) && !string.IsNullOrEmpty(ReleasePrefix) && !string.IsNullOrEmpty(HotfixPrefix); diff --git a/src/Models/GitIgnoreFile.cs b/src/Models/GitIgnoreFile.cs index a23456b67..2f3540e8b 100644 --- a/src/Models/GitIgnoreFile.cs +++ b/src/Models/GitIgnoreFile.cs @@ -1,26 +1,36 @@ -using System.Collections.Generic; -using System.IO; -using Avalonia.Media; +using System; +using System.Collections.Generic; namespace SourceGit.Models { - public class GitIgnoreFile + public record GitIgnoreFile(string DisplayName, string FullPath, string Pattern, bool IsLocalOnly) { - public static readonly List Supported = [new(true), new(false)]; + public static List GetSupported(string repo, string gitDir, string pattern) + { + var supported = new List(); - public bool IsShared { get; set; } - public string File => IsShared ? ".gitignore" : "/info/exclude"; - public string Desc => IsShared ? "Shared" : "Private"; - public IBrush Brush => IsShared ? Brushes.Green : Brushes.Gray; + // .gitignore in repository root. + supported.Add(new(".gitignore", $"{repo}/.gitignore", pattern, false)); - public GitIgnoreFile(bool isShared) - { - IsShared = isShared; - } + // If pattern points to a file/directory that in sub-directory of repository, we should also support .gitignore in that sub-directory. + var normalizedPattern = pattern.Replace('\\', '/').TrimEnd('/'); + var lastDirIdx = normalizedPattern.LastIndexOf('/'); + if (lastDirIdx > 0) + { + var parentDir = normalizedPattern.Substring(0, lastDirIdx); + var overridedPattern = normalizedPattern.Substring(lastDirIdx + 1); + supported.Add(new($"{parentDir}/.gitignore", $"{repo}/{parentDir}/.gitignore", overridedPattern, false)); + } - public string GetFullPath(string repoPath, string gitDir) - { - return IsShared ? Path.Combine(repoPath, ".gitignore") : Path.Combine(gitDir, "info", "exclude"); + // .git/info/exclude in git directory. + var normalizedGitDir = gitDir.Replace('\\', '/'); + var testGitDir = $"{repo}/.git".Replace('\\', '/'); + if (normalizedGitDir.Equals(testGitDir, StringComparison.Ordinal)) + supported.Add(new(".git/info/exclude", $"{normalizedGitDir}/info/exclude", pattern, true)); + else + supported.Add(new(".git/info/exclude", $"{gitDir}/info/exclude", pattern, true)); + + return supported; } } } diff --git a/src/Models/GitVersions.cs b/src/Models/GitVersions.cs index 8aae63a3b..71fb4657e 100644 --- a/src/Models/GitVersions.cs +++ b/src/Models/GitVersions.cs @@ -16,5 +16,15 @@ public static class GitVersions /// The minimal version of Git that supports the `stash push` command with the `--staged` option. /// public static readonly System.Version STASH_PUSH_ONLY_STAGED = new(2, 35, 0); + + /// + /// The minimal version of Git that supports the `git merge-tree --write-tree` command, which is used for testing merge results without actually performing a merge. + /// + public static readonly System.Version TESTING_MERGE = new(2, 38, 0); + + /// + /// The minimal version of Git that supports the `git replay` command. + /// + public static readonly System.Version REPLAY = new(2, 44, 0); } } 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/HistoryFilter.cs b/src/Models/HistoryFilter.cs new file mode 100644 index 000000000..b09f074cb --- /dev/null +++ b/src/Models/HistoryFilter.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public enum FilterType + { + LocalBranch = 0, + LocalBranchFolder, + RemoteBranch, + RemoteBranchFolder, + Tag, + } + + public enum FilterMode + { + None = 0, + Included, + Excluded, + } + + public class 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; + } +} diff --git a/src/Models/HistoryFilterCollection.cs b/src/Models/HistoryFilterCollection.cs deleted file mode 100644 index e2aeb56b1..000000000 --- a/src/Models/HistoryFilterCollection.cs +++ /dev/null @@ -1,258 +0,0 @@ -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/ImageDecoder.cs b/src/Models/ImageDecoder.cs index 6fe0f4287..5ff862eb3 100644 --- a/src/Models/ImageDecoder.cs +++ b/src/Models/ImageDecoder.cs @@ -6,5 +6,6 @@ public enum ImageDecoder Builtin, Pfim, Tiff, + StbImage, } } diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs index bae99ac52..ac7e29d4f 100644 --- a/src/Models/InteractiveRebase.cs +++ b/src/Models/InteractiveRebase.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; namespace SourceGit.Models { @@ -34,10 +37,55 @@ public class InteractiveRebaseJob public string Message { get; set; } = string.Empty; } - public class InteractiveRebaseJobCollection + public partial class InteractiveRebaseJobCollection { public string OrigHead { get; set; } = string.Empty; public string Onto { get; set; } = string.Empty; public List Jobs { get; set; } = new List(); + + public void WriteTodoList(string todoFile) + { + using var writer = new StreamWriter(todoFile); + foreach (var job in Jobs) + { + var code = job.Action switch + { + InteractiveRebaseAction.Pick => 'p', + InteractiveRebaseAction.Edit => 'e', + InteractiveRebaseAction.Reword => 'r', + InteractiveRebaseAction.Squash => 's', + InteractiveRebaseAction.Fixup => 'f', + _ => 'd' + }; + writer.WriteLine($"{code} {job.SHA}"); + } + + writer.Flush(); + } + + public void WriteCommitMessage(string doneFile, string msgFile) + { + var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + if (done.Length == 0) + return; + + var current = done[^1].Trim(); + var match = REG_REBASE_TODO().Match(current); + if (!match.Success) + return; + + var sha = match.Groups[1].Value; + foreach (var job in Jobs) + { + if (job.SHA.StartsWith(sha)) + { + File.WriteAllText(msgFile, job.Message); + return; + } + } + } + + [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,64})(\s+.*)?$")] + private static partial Regex REG_REBASE_TODO(); } } diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs index 702f0630b..86c8167ae 100644 --- a/src/Models/IpcChannel.cs +++ b/src/Models/IpcChannel.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.IO.Pipes; +using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -19,7 +21,7 @@ public IpcChannel() _singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); IsFirstInstance = true; _server = new NamedPipeServerStream( - "SourceGitIPCChannel" + Environment.UserName, + GetPipeName(), PipeDirection.In, -1, PipeTransmissionMode.Byte, @@ -37,7 +39,7 @@ public void SendToFirstInstance(string cmd) { try { - using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) + using (var client = new NamedPipeClientStream(".", GetPipeName(), PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) { client.Connect(1000); if (!client.IsConnected) @@ -67,6 +69,19 @@ public void Dispose() _singletonLock?.Dispose(); } + private static string GetPipeName() + { + // SourceGit does not support multiple instances on macOS, so we can use a fixed pipe name for macOS. + if (OperatingSystem.IsMacOS()) + return "SourceGit"; + + // Windows and Linux can have multiple instances of SourceGit running (portable-mode), so we need to generate a unique pipe name based on the data directory. + var dataDir = Native.OS.DataDir.Replace('\\', '/').TrimEnd('/'); + var hashStr = $"{Environment.UserName}_{dataDir}"; + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(hashStr))).Substring(0, 10); + return $"SG_{hash}"; + } + private async void StartServer() { using var reader = new StreamReader(_server); diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index 027433336..5bdb2431b 100644 --- a/src/Models/Locales.cs +++ b/src/Models/Locales.cs @@ -9,9 +9,11 @@ public class Locale public static readonly List Supported = new List() { new Locale("Deutsch", "de_DE"), + new Locale("Ελληνικά", "el_GR"), new Locale("English", "en_US"), new Locale("Español", "es_ES"), new Locale("Français", "fr_FR"), + new Locale("עברית", "he_IL"), new Locale("Bahasa Indonesia", "id_ID"), new Locale("Italiano", "it_IT"), new Locale("Português (Brasil)", "pt_BR"), diff --git a/src/Models/Notification.cs b/src/Models/Notification.cs index 473947b0f..f033213fb 100644 --- a/src/Models/Notification.cs +++ b/src/Models/Notification.cs @@ -1,8 +1,23 @@ -namespace SourceGit.Models +using System; + +namespace SourceGit.Models { public class Notification { - public bool IsError { get; set; } = false; - public string Message { get; set; } = string.Empty; + public static event Action Raised; + + public string Group { get; set; } + public string Message { get; set; } + public bool IsError { get; set; } + + public static void Send(string group, string message, bool isError = false) + { + Raised?.Invoke(new Notification + { + Group = group, + Message = message, + IsError = isError + }); + } } } diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs deleted file mode 100644 index c38eb674e..000000000 --- a/src/Models/OpenAI.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using CommunityToolkit.Mvvm.ComponentModel; -using OpenAI; -using OpenAI.Chat; - -namespace SourceGit.Models -{ - public partial class OpenAIResponse - { - public OpenAIResponse(Action onUpdate) - { - _onUpdate = onUpdate; - } - - public void Append(string text) - { - var buffer = text; - - if (_thinkTail.Length > 0) - { - _thinkTail.Append(buffer); - buffer = _thinkTail.ToString(); - _thinkTail.Clear(); - } - - buffer = REG_COT().Replace(buffer, ""); - - var startIdx = buffer.IndexOf('<'); - if (startIdx >= 0) - { - if (startIdx > 0) - OnReceive(buffer.Substring(0, startIdx)); - - var endIdx = buffer.IndexOf('>', startIdx + 1); - if (endIdx <= startIdx) - { - if (buffer.Length - startIdx <= 15) - _thinkTail.Append(buffer.AsSpan(startIdx)); - else - OnReceive(buffer.Substring(startIdx)); - } - else if (endIdx < startIdx + 15) - { - var tag = buffer.Substring(startIdx + 1, endIdx - startIdx - 1); - if (_thinkTags.Contains(tag)) - _thinkTail.Append(buffer.AsSpan(startIdx)); - else - OnReceive(buffer.Substring(startIdx)); - } - else - { - OnReceive(buffer.Substring(startIdx)); - } - } - else - { - OnReceive(buffer); - } - } - - public void End() - { - if (_thinkTail.Length > 0) - { - OnReceive(_thinkTail.ToString()); - _thinkTail.Clear(); - } - } - - private void OnReceive(string text) - { - if (!_hasTrimmedStart) - { - text = text.TrimStart(); - if (string.IsNullOrEmpty(text)) - return; - - _hasTrimmedStart = true; - } - - _onUpdate?.Invoke(text); - } - - [GeneratedRegex(@"<(think|thought|thinking|thought_chain)>.*?", RegexOptions.Singleline)] - private static partial Regex REG_COT(); - - private Action _onUpdate = null; - private StringBuilder _thinkTail = new StringBuilder(); - private HashSet _thinkTags = ["think", "thought", "thinking", "thought_chain"]; - private bool _hasTrimmedStart = false; - } - - public class OpenAIService : ObservableObject - { - public string Name - { - get => _name; - set => SetProperty(ref _name, value); - } - - public string Server - { - get => _server; - set => SetProperty(ref _server, value); - } - - public string ApiKey - { - get => _apiKey; - set => SetProperty(ref _apiKey, value); - } - - public bool ReadApiKeyFromEnv - { - get => _readApiKeyFromEnv; - set => SetProperty(ref _readApiKeyFromEnv, value); - } - - public string Model - { - get => _model; - set => SetProperty(ref _model, value); - } - - public bool Streaming - { - get => _streaming; - set => SetProperty(ref _streaming, value); - } - - public string AnalyzeDiffPrompt - { - get => _analyzeDiffPrompt; - set => SetProperty(ref _analyzeDiffPrompt, value); - } - - public string GenerateSubjectPrompt - { - get => _generateSubjectPrompt; - set => SetProperty(ref _generateSubjectPrompt, value); - } - - public OpenAIService() - { - AnalyzeDiffPrompt = """ - You are an expert developer specialist in creating commits. - Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules: - - Do not use any code snippets, imports, file routes or bullets points. - - Do not mention the route of file that has been change. - - Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes. - - Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.". - - Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature". - - Avoid using general terms like "update" or "change", be specific about what was updated or changed. - - Avoid using terms like "The main goal of", just output directly the summary in plain text - """; - - GenerateSubjectPrompt = """ - You are an expert developer specialist in creating commits messages. - Your only goal is to retrieve a single commit message. - Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules: - - Assign the commit {type} according to the next conditions: - feat: Only when adding a new feature. - fix: When fixing a bug. - docs: When updating documentation. - style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic. - test: When adding or updating tests. - chore: When making changes to the build process or auxiliary tools and libraries. - revert: When undoing a previous commit. - refactor: When restructuring code without changing its external behavior, or is any of the other refactor types. - - Do not add any issues numeration, explain your output nor introduce your answer. - - Output directly only one commit message in plain text with the next format: {type}: {commit_message}. - - Be as concise as possible, keep the message under 50 characters. - """; - } - - public async Task ChatAsync(string prompt, string question, CancellationToken cancellation, Action onUpdate) - { - 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 - { - var rsp = new OpenAIResponse(onUpdate); - - if (_streaming) - { - var updates = chatClient.CompleteChatStreamingAsync(messages, null, cancellation); - - await foreach (var update in updates) - { - if (update.ContentUpdate.Count > 0) - rsp.Append(update.ContentUpdate[0].Text); - } - } - else - { - var completion = await chatClient.CompleteChatAsync(messages, null, cancellation); - - if (completion.Value.Content.Count > 0) - rsp.Append(completion.Value.Content[0].Text); - } - - rsp.End(); - } - catch - { - if (!cancellation.IsCancellationRequested) - throw; - } - } - - private string _name; - private string _server; - private string _apiKey; - private bool _readApiKeyFromEnv = false; - private string _model; - private bool _streaming = true; - private string _analyzeDiffPrompt; - private string _generateSubjectPrompt; - } -} diff --git a/src/Models/PatchGenerator.cs b/src/Models/PatchGenerator.cs new file mode 100644 index 000000000..d73b78e1a --- /dev/null +++ b/src/Models/PatchGenerator.cs @@ -0,0 +1,511 @@ +using System.IO; +using System.Text.RegularExpressions; + +namespace SourceGit.Models +{ + public partial class PatchGenerator + { + [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] + private static partial Regex REG_INDICATOR(); + + public bool IsValid => _selection != null; + + public PatchGenerator(DiffOption option, TextDiff diff, int startLine, int endLine, bool isCombined, bool isOldSide) + { + _targetFile = option.Path; + _diff = diff; + _isCombined = isCombined; + _isOldSide = isOldSide; + + var lines = _diff.Lines; + var hasChanges = false; + + var selection = new Selection(); + selection.StartLine = startLine; + selection.EndLine = endLine; + + for (int i = 0; i < startLine; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Added) + selection.IgnoredAdds++; + else if (line.Type == TextDiffLineType.Deleted) + selection.IgnoredDeletes++; + } + + for (int i = startLine; i <= endLine; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Added) + { + if (isCombined || !isOldSide) + { + hasChanges = true; + break; + } + } + else if (line.Type == TextDiffLineType.Deleted) + { + if (isCombined || isOldSide) + { + hasChanges = true; + break; + } + } + } + + if (hasChanges) + _selection = selection; + } + + public void Generate(string saveTo, bool revert) + { + if (_selection == null) + return; + + using var writer = CreateWriter(saveTo, revert); + + if (_isCombined) + GenerateCombined(writer, revert); + else + GenerateForSingleSide(writer, revert); + } + + private void GenerateCombined(StreamWriter writer, bool revert) + { + var lines = _diff.Lines; + var tail = FindTail(revert); + + // If the first line is not indicator. + if (lines[_selection.StartLine].Type != TextDiffLineType.Indicator) + { + var indicator = _selection.StartLine; + for (int i = _selection.StartLine - 1; i >= 0; i--) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + { + indicator = i; + break; + } + } + + var ignoreAdds = 0; + var ignoreRemoves = 0; + for (int i = 0; i < indicator; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Added) + { + ignoreAdds++; + } + else if (line.Type == TextDiffLineType.Deleted) + { + ignoreRemoves++; + } + } + + for (int i = indicator; i < _selection.StartLine; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + { + ProcessIndicator(writer, line, i, ignoreRemoves, ignoreAdds, revert, tail != null); + } + else if (line.Type == TextDiffLineType.Added) + { + if (revert) + Append(writer, ' ', line); + } + else if (line.Type == TextDiffLineType.Deleted) + { + if (!revert) + Append(writer, ' ', line); + } + else if (line.Type == TextDiffLineType.Normal) + { + Append(writer, ' ', line); + } + } + } + + // Outputs the selected lines. + for (int i = _selection.StartLine; i <= _selection.EndLine; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + { + if (!ProcessIndicator(writer, line, i, _selection.IgnoredDeletes, _selection.IgnoredAdds, revert, tail != null)) + break; + } + else if (line.Type == TextDiffLineType.Normal) + { + Append(writer, ' ', line); + } + else if (line.Type == TextDiffLineType.Added) + { + Append(writer, '+', line); + } + else if (line.Type == TextDiffLineType.Deleted) + { + Append(writer, '-', line); + } + } + + if (tail != null) + Append(writer, ' ', tail); + } + + private void GenerateForSingleSide(StreamWriter writer, bool revert) + { + var lines = _diff.Lines; + var tail = FindTail(revert); + + // If the first line is not indicator. + if (lines[_selection.StartLine].Type != TextDiffLineType.Indicator) + { + var indicator = _selection.StartLine; + for (int i = _selection.StartLine - 1; i >= 0; i--) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + { + indicator = i; + break; + } + } + + var ignoreAdds = 0; + var ignoreRemoves = 0; + for (int i = 0; i < indicator; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Added) + { + ignoreAdds++; + } + else if (line.Type == TextDiffLineType.Deleted) + { + ignoreRemoves++; + } + } + + for (int i = indicator; i < _selection.StartLine; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + { + ProcessIndicatorForSingleSide(writer, line, i, ignoreRemoves, ignoreAdds, revert, tail != null); + } + else if (line.Type == TextDiffLineType.Added) + { + if (revert) + Append(writer, ' ', line); + } + else if (line.Type == TextDiffLineType.Deleted) + { + if (!revert) + Append(writer, ' ', line); + } + else if (line.Type == TextDiffLineType.Normal) + { + Append(writer, ' ', line); + } + } + } + + // Outputs the selected lines. + for (int i = _selection.StartLine; i <= _selection.EndLine; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + { + if (!ProcessIndicatorForSingleSide(writer, line, i, _selection.IgnoredDeletes, _selection.IgnoredAdds, revert, tail != null)) + break; + } + else if (line.Type == TextDiffLineType.Normal) + { + Append(writer, ' ', line); + } + else if (line.Type == TextDiffLineType.Added) + { + if (_isOldSide) + { + if (revert) + Append(writer, ' ', line); + else + _selection.IgnoredAdds++; + } + else + { + Append(writer, '+', line); + } + } + else if (line.Type == TextDiffLineType.Deleted) + { + if (_isOldSide) + { + Append(writer, '-', line); + } + else + { + if (!revert) + Append(writer, ' ', line); + else + _selection.IgnoredDeletes++; + } + } + } + + if (tail != null) + Append(writer, ' ', tail); + } + + private StreamWriter CreateWriter(string saveTo, bool revert) + { + var writer = new StreamWriter(saveTo) { NewLine = "\n" }; + writer.WriteLine($"diff --git \"a/{_targetFile}\" \"b/{_targetFile}\""); + + if (_diff.OldMode == 0) + { + if (_diff.NewMode == 0 || revert) + { + writer.WriteLine($"index {_diff.OldHash}..{_diff.NewHash}"); + writer.WriteLine($"--- a/{_targetFile}"); + } + else + { + writer.WriteLine($"new file mode {_diff.NewMode}"); + writer.WriteLine($"--- /dev/null"); + } + } + else if (_diff.NewMode == 0) + { + writer.WriteLine($"index {_diff.OldHash}..{_diff.NewHash} {_diff.OldMode}"); + writer.WriteLine($"--- a/{_targetFile}"); + } + else + { + writer.WriteLine($"--- a/{_targetFile}"); + } + + writer.WriteLine($"+++ b/{_targetFile}"); + return writer; + } + + private TextDiffLine FindTail(bool revert) + { + var lines = _diff.Lines; + + if (_selection.EndLine < lines.Count - 1) + { + var lastLine = lines[_selection.EndLine]; + if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) + { + for (int i = _selection.EndLine + 1; i < lines.Count; i++) + { + var line = lines[i]; + if (line.Type == TextDiffLineType.Indicator) + break; + + if (revert) + { + if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) + return line; + } + else + { + if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) + return line; + } + } + } + } + + return null; + } + + private void Append(StreamWriter writer, char prefix, TextDiffLine line) + { + writer.Flush(); + + writer.BaseStream.WriteByte((byte)prefix); + writer.BaseStream.Write(line.RawContent); + writer.BaseStream.WriteByte((byte)'\n'); + + if (line.NoNewLineEndOfFile) + writer.WriteLine("\\ No newline at end of file"); + } + + private bool ProcessIndicator(StreamWriter writer, TextDiffLine indicator, int idx, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed) + { + var lines = _diff.Lines; + var start = _selection.StartLine; + var end = _selection.EndLine; + var match = REG_INDICATOR().Match(indicator.Content); + var oldStart = int.Parse(match.Groups[1].Value); + var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; + var oldCount = 0; + var newCount = 0; + + for (int i = idx + 1; i <= end; i++) + { + var test = lines[i]; + if (test.Type == TextDiffLineType.Indicator) + break; + + if (test.Type == TextDiffLineType.Normal) + { + oldCount++; + newCount++; + } + else if (test.Type == TextDiffLineType.Added) + { + if (i < start) + { + if (revert) + { + newCount++; + oldCount++; + } + } + else + { + newCount++; + } + + if (i == end && tailed) + { + newCount++; + oldCount++; + } + } + else if (test.Type == TextDiffLineType.Deleted) + { + if (i < start) + { + if (!revert) + { + newCount++; + oldCount++; + } + } + else + { + oldCount++; + } + + if (i == end && tailed) + { + newCount++; + oldCount++; + } + } + } + + if (oldCount == 0 && newCount == 0) + return false; + + writer.WriteLine($"@@ -{oldStart},{oldCount} +{newStart},{newCount} @@"); + return true; + } + + private bool ProcessIndicatorForSingleSide(StreamWriter writer, TextDiffLine indicator, int idx, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed) + { + var lines = _diff.Lines; + var start = _selection.StartLine; + var end = _selection.EndLine; + var match = REG_INDICATOR().Match(indicator.Content); + var oldStart = int.Parse(match.Groups[1].Value); + var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; + var oldCount = 0; + var newCount = 0; + + for (int i = idx + 1; i <= end; i++) + { + var test = lines[i]; + if (test.Type == TextDiffLineType.Indicator) + break; + + if (test.Type == TextDiffLineType.Normal) + { + oldCount++; + newCount++; + } + else if (test.Type == TextDiffLineType.Added) + { + if (i < start || _isOldSide) + { + if (revert) + { + newCount++; + oldCount++; + } + } + else + { + newCount++; + } + + if (i == end && tailed) + { + newCount++; + oldCount++; + } + } + else if (test.Type == TextDiffLineType.Deleted) + { + if (i < start) + { + if (!revert) + { + newCount++; + oldCount++; + } + } + else + { + if (_isOldSide) + { + oldCount++; + } + else + { + if (!revert) + { + newCount++; + oldCount++; + } + } + } + + if (i == end && tailed) + { + newCount++; + oldCount++; + } + } + } + + if (oldCount == 0 && newCount == 0) + return false; + + writer.WriteLine($"@@ -{oldStart},{oldCount} +{newStart},{newCount} @@"); + return true; + } + + private class Selection + { + public int StartLine { get; set; } = 0; + public int EndLine { get; set; } = 0; + public int IgnoredAdds { get; set; } = 0; + public int IgnoredDeletes { get; set; } = 0; + } + + private string _targetFile = null; + private TextDiff _diff = null; + private bool _isCombined = false; + private bool _isOldSide = false; + private Selection _selection = null; + } +} diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index 3550a4e56..12681b4b4 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -31,6 +31,7 @@ public partial class Remote public string Name { get; set; } public string URL { get; set; } + public bool DisableAutoFetch { get; set; } public static bool IsSSH(string url) { @@ -64,21 +65,24 @@ public bool TryGetVisitURL(out string url) { url = null; - if (URL.StartsWith("http", StringComparison.Ordinal)) + if (URL.StartsWith("http://", StringComparison.Ordinal) || URL.StartsWith("https://", StringComparison.Ordinal)) { - var uri = new Uri(URL.EndsWith(".git", StringComparison.Ordinal) ? URL.Substring(0, URL.Length - 4) : URL); + var trimmed = URL.EndsWith(".git", StringComparison.Ordinal) ? URL.Substring(0, URL.Length - 4) : URL; + var uri = new Uri(trimmed); if (uri.Port != 80 && uri.Port != 443) - url = $"{uri.Scheme}://{uri.Host}:{uri.Port}{uri.LocalPath}"; + url = $"{uri.Scheme}://{uri.Host}:{uri.Port}{uri.AbsolutePath}"; else - url = $"{uri.Scheme}://{uri.Host}{uri.LocalPath}"; - + url = $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}"; return true; } 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; } @@ -94,7 +98,6 @@ public bool TryGetCreatePullRequestURL(out string url, string mergeBranch) 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)) @@ -127,7 +130,8 @@ public bool TryGetCreatePullRequestURL(out string url, string mergeBranch) return true; } - if (host.Contains("azure.com", StringComparison.Ordinal)) + if (host.Contains("azure.com", StringComparison.Ordinal) || + host.Contains("visualstudio.com", StringComparison.Ordinal)) { url = $"{baseURL}/pullrequestcreate?sourceRef={encodedBranch}"; return true; diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index b0d52d50a..506e6ded7 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,4 +1,11 @@ +using System; using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + using Avalonia.Collections; namespace SourceGit.Models @@ -11,107 +18,35 @@ public string DefaultRemote set; } = string.Empty; - public HistoryShowFlags HistoryShowFlags - { - get; - set; - } = HistoryShowFlags.None; - - public bool EnableTopoOrderInHistories - { - get; - set; - } = false; - - public bool OnlyHighlightCurrentBranchInHistories - { - get; - set; - } = false; - - public BranchSortMode LocalBranchSortMode - { - get; - set; - } = BranchSortMode.Name; - - public BranchSortMode RemoteBranchSortMode - { - get; - set; - } = BranchSortMode.Name; - - public TagSortMode TagSortMode - { - get; - set; - } = TagSortMode.CreatorDate; - - public bool IncludeUntrackedInLocalChanges - { - get; - set; - } = true; - - public bool EnableForceOnFetch - { - get; - set; - } = false; - - public bool FetchAllRemotes - { - get; - set; - } = false; - - public bool FetchWithoutTags - { - get; - set; - } = false; - - public bool PreferRebaseInsteadOfMerge - { - get; - set; - } = true; - - public bool CheckSubmodulesOnPush + public int PreferredMergeMode { get; set; - } = true; + } = 0; - public bool PushAllTags + public string ConventionalTypesOverride { get; set; - } = false; + } = string.Empty; - public bool PushToRemoteWhenCreateTag + public bool EnableRecursiveWhenAutoUpdatingSubmodules { get; set; } = true; - public bool PushToRemoteWhenDeleteTag + public bool AskBeforeAutoUpdatingSubmodules { get; set; } = false; - public bool CheckoutBranchOnCreateBranch - { - get; - set; - } = true; - - public bool UpdateSubmodulesOnCheckoutBranch + public string PreferredOpenAIService { get; set; - } = true; + } = "---"; public AvaloniaList CommitTemplates { @@ -119,137 +54,66 @@ public AvaloniaList CommitTemplates set; } = []; - public AvaloniaList CommitMessages - { - get; - set; - } = []; - public AvaloniaList CustomActions { get; set; } = []; - public bool EnableAutoFetch - { - get; - set; - } = false; - - public int AutoFetchInterval - { - get; - set; - } = 10; - - public bool EnableSignOffForCommit - { - get; - set; - } = false; - - public bool NoVerifyOnCommit - { - get; - set; - } = false; - - public bool IncludeUntrackedWhenStash - { - get; - set; - } = true; - - public bool OnlyStagedWhenStash - { - get; - set; - } = false; - - public int ChangesAfterStashing - { - get; - set; - } = 0; - - public string PreferredOpenAIService - { - get; - set; - } = "---"; - - public bool IsLocalBranchesExpandedInSideBar - { - get; - set; - } = true; - - public bool IsRemotesExpandedInSideBar - { - get; - set; - } = false; - - public bool IsTagsExpandedInSideBar - { - get; - set; - } = false; - - public bool IsSubmodulesExpandedInSideBar + public static RepositorySettings Get(string gitCommonDir) { - get; - set; - } = false; + var fileInfo = new FileInfo(Path.Combine(gitCommonDir, "sourcegit.settings")); + var fullpath = fileInfo.FullName; + if (_cache.TryGetValue(fullpath, out var setting)) + return setting; - public bool IsWorktreeExpandedInSideBar - { - get; - set; - } = false; - - public List ExpandedBranchNodesInSideBar - { - get; - set; - } = []; - - public int PreferredMergeMode - { - get; - set; - } = 0; + if (!File.Exists(fullpath)) + { + setting = new(); + } + else + { + try + { + using var stream = File.OpenRead(fullpath); + setting = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.RepositorySettings); + } + catch + { + setting = new(); + } + } - public string LastCommitMessage - { - get; - set; - } = string.Empty; + // Serialize setting again to make sure there are no unnecessary whitespaces. + Task.Run(() => + { + var formatted = JsonSerializer.Serialize(setting, JsonCodeGen.Default.RepositorySettings); + setting._orgHash = HashContent(formatted); + }); - public string ConventionalTypesOverride - { - get; - set; - } = string.Empty; + setting._file = fullpath; + _cache.Add(fullpath, setting); + return setting; + } - public void PushCommitMessage(string message) + public void Save() { - message = message.Trim().ReplaceLineEndings("\n"); - var existIdx = CommitMessages.IndexOf(message); - if (existIdx == 0) - return; - - if (existIdx > 0) + try { - CommitMessages.Move(existIdx, 0); - return; + var content = JsonSerializer.Serialize(this, JsonCodeGen.Default.RepositorySettings); + var hash = HashContent(content); + if (!hash.Equals(_orgHash, StringComparison.Ordinal)) + { + var tmpfile = $"{_file}.tmp"; + File.WriteAllText(tmpfile, content); + File.Move(tmpfile, _file, true); + _orgHash = hash; + } + } + catch + { + // Ignore save errors } - - if (CommitMessages.Count > 9) - CommitMessages.RemoveRange(9, CommitMessages.Count - 9); - - CommitMessages.Insert(0, message); } public CustomAction AddNewCustomAction() @@ -278,5 +142,15 @@ public void MoveCustomActionDown(CustomAction act) if (idx < CustomActions.Count - 1) CustomActions.Move(idx + 1, idx); } + + private static string HashContent(string source) + { + var hash = MD5.HashData(Encoding.Default.GetBytes(source)); + return Convert.ToHexStringLower(hash); + } + + private static Dictionary _cache = new(); + private string _file = string.Empty; + private string _orgHash = string.Empty; } } diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs new file mode 100644 index 000000000..723fe900b --- /dev/null +++ b/src/Models/RepositoryUIStates.cs @@ -0,0 +1,514 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using Avalonia.Collections; + +namespace SourceGit.Models +{ + public class RepositoryUIStates + { + public HistoryShowFlags HistoryShowFlags + { + get; + set; + } = HistoryShowFlags.None; + + public bool IsAuthorColumnVisibleInHistory + { + get; + set; + } = true; + + public bool IsSHAColumnVisibleInHistory + { + get; + set; + } = true; + + public bool IsAuthorTimeColumnVisibleInHistory + { + get; + set; + } = false; + + public bool IsCommitTimeColumnVisibleInHistory + { + get; + set; + } = true; + + public double AuthorColumnWidth + { + get; + set; + } = 120; + + public bool EnableTopoOrderInHistory + { + get; + set; + } = false; + + public CommitGraphHighlighting GraphHighlighting + { + get; + set; + } = CommitGraphHighlighting.All; + + public BranchSortMode LocalBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public BranchSortMode RemoteBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public bool ShowTagsAsTree + { + get; + set; + } = false; + + public TagSortMode TagSortMode + { + get; + set; + } = TagSortMode.CreatorDate; + + public bool ShowSubmodulesAsTree + { + get; + set; + } = false; + + public bool IncludeUntrackedInLocalChanges + { + get; + set; + } = true; + + public bool EnableForceOnFetch + { + get; + set; + } = false; + + public bool FetchAllRemotes + { + get; + set; + } = false; + + public bool FetchWithoutTags + { + get; + set; + } = false; + + public bool PreferRebaseInsteadOfMerge + { + get; + set; + } = true; + + public bool CheckSubmodulesOnPush + { + get; + set; + } = true; + + public bool PushAllTags + { + get; + set; + } = false; + + public bool CreateAnnotatedTag + { + get; + set; + } = true; + + public bool PushToRemoteWhenCreateTag + { + get; + set; + } = true; + + public bool PushToRemoteWhenDeleteTag + { + get; + set; + } = false; + + public bool CheckoutBranchOnCreateBranch + { + get; + set; + } = true; + + public bool EnableSignOffForCommit + { + get; + set; + } = false; + + public bool NoVerifyOnCommit + { + get; + set; + } = false; + + public bool IncludeUntrackedWhenStash + { + get; + set; + } = true; + + public bool OnlyStagedWhenStash + { + get; + set; + } = false; + + public int ChangesAfterStashing + { + get; + set; + } = 0; + + public bool IsLocalBranchesExpandedInSideBar + { + get; + set; + } = true; + + public bool IsRemotesExpandedInSideBar + { + get; + set; + } = false; + + public bool IsTagsExpandedInSideBar + { + get; + set; + } = false; + + public bool IsSubmodulesExpandedInSideBar + { + get; + set; + } = false; + + public bool IsWorktreeExpandedInSideBar + { + get; + set; + } = false; + + public List ExpandedBranchNodesInSideBar + { + get; + set; + } = []; + + public string LastCommitMessage + { + get; + set; + } = string.Empty; + + public AvaloniaList HistoryFilters + { + get; + set; + } = []; + + public bool IsHistoryFiltersCollapsed + { + get; + set; + } = false; + + public List RecentCommitMessages + { + get; + set; + } = []; + + public static RepositoryUIStates Load(string gitDir) + { + var fileInfo = new FileInfo(Path.Combine(gitDir, "sourcegit.uistates")); + var fullpath = fileInfo.FullName; + + RepositoryUIStates states; + if (!File.Exists(fullpath)) + { + states = new RepositoryUIStates(); + } + else + { + try + { + using var stream = File.OpenRead(fullpath); + states = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.RepositoryUIStates); + } + catch + { + states = new RepositoryUIStates(); + } + } + + states._file = fullpath; + return states; + } + + public void Save() + { + try + { + var content = JsonSerializer.Serialize(this, JsonCodeGen.Default.RepositoryUIStates); + var tmpfile = $"{_file}.tmp"; + File.WriteAllText(tmpfile, content); + File.Move(tmpfile, _file, true); + } + catch + { + // Ignore save errors + } + } + + public FilterMode GetHistoryFilterMode(string pattern = null) + { + if (string.IsNullOrEmpty(pattern)) + return HistoryFilters.Count == 0 ? FilterMode.None : HistoryFilters[0].Mode; + + foreach (var filter in HistoryFilters) + { + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return filter.Mode; + } + + return FilterMode.None; + } + + public Dictionary GetHistoryFiltersMap() + { + var map = new Dictionary(); + foreach (var filter in HistoryFilters) + map.Add(filter.Pattern, filter.Mode); + return map; + } + + public bool UpdateHistoryFilters(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 HistoryFilters) + { + if (filter.Mode != mode) + { + clear = true; + break; + } + } + + if (clear) + { + HistoryFilters.Clear(); + HistoryFilters.Add(new HistoryFilter(pattern, type, mode)); + return true; + } + } + else + { + for (int i = 0; i < HistoryFilters.Count; i++) + { + var filter = HistoryFilters[i]; + if (filter.Type == type && filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + HistoryFilters.RemoveAt(i); + return true; + } + } + + return false; + } + + foreach (var filter in HistoryFilters) + { + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return false; + } + + HistoryFilters.Add(new HistoryFilter(pattern, type, mode)); + return true; + } + + public void RemoveHistoryFilter(string pattern, FilterType type) + { + foreach (var filter in HistoryFilters) + { + if (filter.Type == type && filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + HistoryFilters.Remove(filter); + break; + } + } + } + + public void RenameBranchFilter(string oldName, string newName) + { + foreach (var filter in HistoryFilters) + { + if (filter.Type == FilterType.LocalBranch && + filter.Pattern.Equals(oldName, StringComparison.Ordinal)) + { + filter.Pattern = $"refs/heads/{newName}"; + break; + } + } + } + + public void RemoveBranchFiltersByPrefix(string pattern) + { + var dirty = new List(); + var prefix = $"{pattern}/"; + + foreach (var filter in HistoryFilters) + { + if (filter.Type == FilterType.Tag) + continue; + + if (filter.Pattern.StartsWith(prefix, StringComparison.Ordinal)) + dirty.Add(filter); + } + + foreach (var filter in dirty) + HistoryFilters.Remove(filter); + } + + public string BuildHistoryParams(string gitDir) + { + var builder = new StringBuilder(); + + if (EnableTopoOrderInHistory) + builder.Append("--topo-order "); + else + builder.Append("--date-order "); + + if (HistoryShowFlags.HasFlag(HistoryShowFlags.Reflog)) + builder.Append("--reflog "); + + if (HistoryShowFlags.HasFlag(HistoryShowFlags.FirstParentOnly)) + builder.Append("--first-parent "); + + if (HistoryShowFlags.HasFlag(HistoryShowFlags.SimplifyByDecoration)) + builder.Append("--simplify-by-decoration "); + + var mode = GetHistoryFilterMode(); + if (mode == FilterMode.None) + builder.Append("--branches --remotes --tags HEAD"); + else if (mode == FilterMode.Included) + BuildHistoryParamsForIncluded(builder); + else + BuildHistoryParamsForExcluded(builder, gitDir); + + return builder.ToString(); + } + + private void BuildHistoryParamsForIncluded(StringBuilder builder) + { + foreach (var filter in HistoryFilters) + { + if (filter.Type == FilterType.LocalBranch) + builder.Append(filter.Pattern).Append(' '); + else if (filter.Type == FilterType.LocalBranchFolder) + builder.Append($"--branches={filter.Pattern.AsSpan(11)}/* "); + else if (filter.Type == FilterType.RemoteBranch) + builder.Append(filter.Pattern).Append(' '); + else if (filter.Type == FilterType.RemoteBranchFolder) + builder.Append($"--remotes={filter.Pattern.AsSpan(13)}/* "); + else if (filter.Type == FilterType.Tag) + builder.Append($"refs/tags/{filter.Pattern} "); + } + } + + private void BuildHistoryParamsForExcluded(StringBuilder builder, string gitDir) + { + var excludedBranches = new List(); + var excludedRemotes = new List(); + var excludedTags = new List(); + foreach (var filter in HistoryFilters) + { + if (filter.Type == FilterType.LocalBranch) + excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\" "); + else if (filter.Type == FilterType.LocalBranchFolder) + excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\" "); + else if (filter.Type == FilterType.RemoteBranch) + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\" "); + else if (filter.Type == FilterType.RemoteBranchFolder) + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\" "); + else if (filter.Type == FilterType.Tag) + excludedTags.Add($"--exclude=\"{filter.Pattern}\" --decorate-refs-exclude=\"refs/tags/{filter.Pattern}\" "); + } + + foreach (var b in excludedBranches) + builder.Append(b); + + builder.Append("--branches "); + + var isInProgress = File.Exists(Path.Combine(gitDir, "CHERRY_PICK_HEAD")) || + Directory.Exists(Path.Combine(gitDir, "rebase-merge")) || + Directory.Exists(Path.Combine(gitDir, "rebase-apply")) || + File.Exists(Path.Combine(gitDir, "REVERT_HEAD")) || + File.Exists(Path.Combine(gitDir, "MERGE_HEAD")); + if (isInProgress) + builder.Append("HEAD "); + + foreach (var r in excludedRemotes) + builder.Append(r); + + builder.Append("--exclude=origin/HEAD --remotes "); + + foreach (var t in excludedTags) + builder.Append(t); + + builder.Append("--tags "); + } + + public void AddRecentCommitMessage(string message) + { + message = message.Trim().ReplaceLineEndings("\n"); + var existIdx = RecentCommitMessages.IndexOf(message); + if (existIdx == 0) + return; + + if (existIdx > 0) + { + RecentCommitMessages.RemoveAt(existIdx); + RecentCommitMessages.Insert(0, message); + return; + } + + if (RecentCommitMessages.Count > 9) + RecentCommitMessages.RemoveRange(9, RecentCommitMessages.Count - 9); + + RecentCommitMessages.Insert(0, message); + } + + private string _file = string.Empty; + } +} diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index 29a23efa7..9a9fe7d20 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -39,5 +39,6 @@ public class RevisionSubmodule { public Commit Commit { get; set; } = null; public CommitFullMessage FullMessage { get; set; } = null; + public int UncommittedChanges { get; set; } = 0; } } diff --git a/src/Models/SelfUpdate.cs b/src/Models/SelfUpdate.cs index 05fa61247..9cf95a148 100644 --- a/src/Models/SelfUpdate.cs +++ b/src/Models/SelfUpdate.cs @@ -12,24 +12,28 @@ public class Version [JsonPropertyName("tag_name")] public string TagName { get; set; } + [JsonPropertyName("published_at")] + public DateTime PublishedAt { get; set; } + [JsonPropertyName("body")] public string Body { get; set; } - public bool IsNewVersion + [JsonIgnore] + public System.Version CurrentVersion { get; } + + [JsonIgnore] + public string CurrentVersionStr => $"v{CurrentVersion.Major}.{CurrentVersion.Minor:D2}"; + + [JsonIgnore] + public bool IsNewVersion => CurrentVersion.CompareTo(new System.Version(TagName.Substring(1))) < 0; + + [JsonIgnore] + public string ReleaseDateStr => DateTimeFormat.Format(PublishedAt, true); + + public Version() { - get - { - try - { - System.Version version = new System.Version(TagName.Substring(1)); - System.Version current = Assembly.GetExecutingAssembly().GetName().Version!; - return current.CompareTo(version) < 0; - } - catch - { - return false; - } - } + var assembly = Assembly.GetExecutingAssembly().GetName(); + CurrentVersion = assembly.Version ?? new System.Version(); } } diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index 84c9ae066..e8e585475 100644 --- a/src/Models/ShellOrTerminal.cs +++ b/src/Models/ShellOrTerminal.cs @@ -44,7 +44,10 @@ static ShellOrTerminal() new ShellOrTerminal("iterm2", "iTerm", "iTerm"), new ShellOrTerminal("warp", "Warp", "Warp"), new ShellOrTerminal("ghostty", "Ghostty", "Ghostty"), - new ShellOrTerminal("kitty", "kitty", "kitty") + new ShellOrTerminal("kitty", "kitty", "kitty"), + new ShellOrTerminal("otty", "Otty", "Otty"), + new ShellOrTerminal("cmux", "cmux", "cmux"), + new ShellOrTerminal("mux0", "mux0", "mux0"), }; } else @@ -60,6 +63,7 @@ static ShellOrTerminal() new ShellOrTerminal("foot", "Foot", "foot"), new ShellOrTerminal("wezterm", "WezTerm", "wezterm", "start --cwd ."), new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis", "--new-window --working-directory=."), + new ShellOrTerminal("ghostty", "Ghostty", "ghostty"), new ShellOrTerminal("kitty", "kitty", "kitty"), new ShellOrTerminal("custom", "Custom", ""), }; diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index bc01e9db1..93439a40a 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SourceGit.Models { @@ -10,24 +9,7 @@ public class Stash public List Parents { get; set; } = []; public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; - - public string Subject - { - get - { - return Message.Split('\n', 2)[0].Trim(); - } - } - - public string TimeStr - { - get - { - return DateTime.UnixEpoch - .AddSeconds(Time) - .ToLocalTime() - .ToString(DateTimeFormat.Active.DateTime); - } - } + public string Subject => Message.Split('\n', 2)[0].Trim(); + public string UntrackedParent => EmptyTreeHash.Guess(SHA); } } diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index 700c9311b..4ef9927fc 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -2,18 +2,11 @@ using System.Collections.Generic; using System.Globalization; -using LiveChartsCore; -using LiveChartsCore.Defaults; -using LiveChartsCore.SkiaSharpView; -using LiveChartsCore.SkiaSharpView.Painting; - -using SkiaSharp; - namespace SourceGit.Models { public enum StatisticsMode { - All, + All = 0, ThisMonth, ThisWeek, } @@ -24,45 +17,103 @@ public class StatisticsAuthor(User user, int count) public int Count { get; set; } = count; } + public class StatisticsSamples + { + public DateTime StartTime { get; } + public DateTime EndTime { get; } + public int Count { get; } + public int MaxValue { get; } + public bool HasSpecialUser => _user != null; + + public StatisticsSamples(StatisticsMode mode, DateTime start, DateTime end, Dictionary all, int maxValue) + { + _mode = mode; + _all = all; + + StartTime = start; + EndTime = end; + + if (maxValue < 8) + MaxValue = 8; + else if (maxValue < 16) + MaxValue = 16; + else if (maxValue < 24) + MaxValue = 24; + else + MaxValue = (int)(Math.Floor(maxValue / 6.0) * 8.0); + + if (maxValue == 0) + Count = 0; + else + Count = mode switch + { + StatisticsMode.All => (end.Year - start.Year) * 12 + (end.Month - start.Month) + 1, + _ => all.Count, + }; + } + + public void WithUser(Dictionary user) + { + _user = user; + } + + public (string, int, int) GetSample(DateTime time) + { + var label = _mode switch + { + StatisticsMode.All => time.ToString("yyyy/MM"), + StatisticsMode.ThisMonth => time.ToString("MM/dd"), + _ => s_weekdays[(int)time.DayOfWeek] + }; + + var total = _all.GetValueOrDefault(time, 0); + var user = _user?.GetValueOrDefault(time, 0) ?? 0; + return (label, total, user); + } + + public DateTime NextSampleTime(DateTime time) + { + if (_mode != StatisticsMode.All) + return time.AddDays(-1); + + if (time.Month == 1) + return new DateTime(time.Year - 1, 12, 1).ToLocalTime().Date; + + return new DateTime(time.Year, time.Month - 1, 1).ToLocalTime().Date; + } + + private static readonly string[] s_weekdays = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]; + private StatisticsMode _mode; + private Dictionary _all; + private Dictionary _user; + } + public class StatisticsReport { + public StatisticsMode Mode { get; } public int Total { get; set; } = 0; public List Authors { get; set; } = new(); - public List Series { get; set; } = new(); - public List XAxes { get; set; } = new(); - public List YAxes { get; set; } = new(); - public StatisticsAuthor SelectedAuthor { get => _selectedAuthor; set => ChangeAuthor(value); } public StatisticsReport(StatisticsMode mode, DateTime start) { - _mode = mode; - - YAxes.Add(new Axis() - { - TextSize = 10, - MinLimit = 0, - SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 } - }); + Mode = mode; if (mode == StatisticsMode.ThisWeek) { - for (int i = 0; i < 7; i++) - _mapSamples.Add(start.AddDays(i), 0); + _minSampleTime = start; + _maxSampleTime = start.AddDays(6); - XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => WEEKDAYS[(int)v.DayOfWeek]) { TextSize = 10 }); + for (int i = 0; i < 7; i++) + _all.Add(start.AddDays(i), 0); } else if (mode == StatisticsMode.ThisMonth) { var now = DateTime.Now; var maxDays = DateTime.DaysInMonth(now.Year, now.Month); + _minSampleTime = start; + _maxSampleTime = start.AddDays(maxDays - 1); for (int i = 0; i < maxDays; i++) - _mapSamples.Add(start.AddDays(i), 0); - - XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => $"{v:MM/dd}") { TextSize = 10 }); - } - else - { - XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(30), v => $"{v:yyyy/MM}") { TextSize = 10 }); + _all.Add(start.AddDays(i), 0); } } @@ -71,15 +122,24 @@ public void AddCommit(DateTime time, User author) Total++; DateTime normalized; - if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth) + if (Mode == StatisticsMode.ThisWeek || Mode == StatisticsMode.ThisMonth) + { normalized = time.Date; + } else - normalized = new DateTime(time.Year, time.Month, 1).ToLocalTime(); + { + normalized = new DateTime(time.Year, time.Month, 1).ToLocalTime().Date; + + if (normalized < _minSampleTime) + _minSampleTime = normalized; + if (normalized > _maxSampleTime) + _maxSampleTime = normalized; + } - if (_mapSamples.TryGetValue(normalized, out var vs)) - _mapSamples[normalized] = vs + 1; + if (_all.TryGetValue(normalized, out var vs)) + _all[normalized] = vs + 1; else - _mapSamples.Add(normalized, 1); + _all.Add(normalized, 1); if (_mapUsers.TryGetValue(author, out var vu)) _mapUsers[author] = vu + 1; @@ -95,10 +155,9 @@ public void AddCommit(DateTime time, User author) } else { - _mapUserSamples.Add(author, new Dictionary - { - { normalized, 1 } - }); + var added = new Dictionary(); + added.Add(normalized, 1); + _mapUserSamples.Add(author, added); } } @@ -107,76 +166,30 @@ public void Complete() foreach (var kv in _mapUsers) Authors.Add(new StatisticsAuthor(kv.Key, kv.Value)); - Authors.Sort((l, r) => r.Count - l.Count); - - var samples = new List(); - foreach (var kv in _mapSamples) - samples.Add(new DateTimePoint(kv.Key, kv.Value)); - - Series.Add( - new ColumnSeries() - { - Values = samples, - Stroke = null, - Fill = null, - Padding = 1, - } - ); - _mapUsers.Clear(); - _mapSamples.Clear(); - } - - public void ChangeColor(uint color) - { - _fillColor = color; - - var fill = new SKColor(color); - - if (Series.Count > 0 && Series[0] is ColumnSeries total) - total.Fill = new SolidColorPaint(_selectedAuthor == null ? fill : fill.WithAlpha(51)); - - if (Series.Count > 1 && Series[1] is ColumnSeries user) - user.Fill = new SolidColorPaint(fill); - } - - public void ChangeAuthor(StatisticsAuthor author) - { - if (author == _selectedAuthor) - return; + Authors.Sort((l, r) => r.Count - l.Count); - _selectedAuthor = author; - Series.RemoveRange(1, Series.Count - 1); - if (author == null || !_mapUserSamples.TryGetValue(author.User, out var userSamples)) + foreach (var kv in _all) { - ChangeColor(_fillColor); - return; + if (kv.Value > _maxSampleValue) + _maxSampleValue = kv.Value; } + } - var samples = new List(); - foreach (var kv in userSamples) - samples.Add(new DateTimePoint(kv.Key, kv.Value)); - - Series.Add( - new ColumnSeries() - { - Values = samples, - Stroke = null, - Fill = null, - Padding = 1, - } - ); - - ChangeColor(_fillColor); + public StatisticsSamples GetSamples(StatisticsAuthor withUser) + { + var samples = new StatisticsSamples(Mode, _minSampleTime, _maxSampleTime, _all, _maxSampleValue); + if (withUser != null && _mapUserSamples.TryGetValue(withUser.User, out var userSamples)) + samples.WithUser(userSamples); + return samples; } - private static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; - private StatisticsMode _mode; + private DateTime _minSampleTime = DateTime.MaxValue; + private DateTime _maxSampleTime = DateTime.MinValue; + private int _maxSampleValue = 0; + private Dictionary _all = new(); private Dictionary _mapUsers = new(); - private Dictionary _mapSamples = new(); private Dictionary> _mapUserSamples = new(); - private StatisticsAuthor _selectedAuthor = null; - private uint _fillColor = 255; } public class Statistics @@ -207,7 +220,7 @@ public void AddCommit(string author, double timestamp) _users.Add(email, user); } - var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); + var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().Date; if (time >= _thisWeekStart) Week.AddCommit(time, user); diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 9e1654575..5e3ed7b41 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -1,6 +1,4 @@ -using System; - -namespace SourceGit.Models +namespace SourceGit.Models { public enum TagSortMode { @@ -16,10 +14,5 @@ public class Tag public User Creator { get; set; } = null; public ulong CreatorDate { get; set; } = 0; public string Message { get; set; } = string.Empty; - - public string CreatorDateStr - { - get => DateTime.UnixEpoch.AddSeconds(CreatorDate).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); - } } } diff --git a/src/Models/TextInlineChange.cs b/src/Models/TextInlineChange.cs index bc1873e25..e49a6d032 100644 --- a/src/Models/TextInlineChange.cs +++ b/src/Models/TextInlineChange.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; namespace SourceGit.Models { @@ -26,6 +27,13 @@ private enum Edit AddedLeft, } + private enum CharCategory : byte + { + Other, // default: whitespace, control, punctuation, symbols, etc. + Letter, // Ll/Lu/Lt/Lm + digit: ASCII and euro letters (latin, greek, cyrillic, etc.) + OtherLetter, // Lo: CJK, hiragana, katakana, hangul, Thai, Arabic, etc. + } + private class EditResult { public Edit State; @@ -100,22 +108,25 @@ private static List MakeChunks(Dictionary hashes, string tex var start = 0; var size = text.Length; var chunks = new List(); - var delims = new HashSet(" \t+-*/=!,:;.'\"/?|&#@%`<>()[]{}\\".ToCharArray()); + if (size == 0) + return chunks; - for (int i = 0; i < size; i++) + var prev = GetCategory(text[0]); + + for (var i = 1; i < size; i++) { var ch = text[i]; - if (delims.Contains(ch)) + var category = GetCategory(ch); + if (prev != category || category == CharCategory.Other) { - if (start != i) - AddChunk(chunks, hashes, text.Substring(start, i - start), start); - AddChunk(chunks, hashes, text.Substring(i, 1), i); - start = i + 1; + AddChunk(chunks, hashes, text[start..i], start); + start = i; } + prev = category; } if (start < size) - AddChunk(chunks, hashes, text.Substring(start), start); + AddChunk(chunks, hashes, text[start..], start); return chunks; } @@ -302,5 +313,33 @@ private static void AddChunk(List chunks, Dictionary hashes, } chunks.Add(new Chunk(hash, start, data.Length)); } + + private static CharCategory[] BuildCategoryCache() + { + // Pre-compute category for all char values. + // All entries default to Other (0). + var cache = new CharCategory[65536]; + for (int i = 0; i < 65536; i++) + { + var ch = (char)i; + // Unicode Lo: CJK, hiragana, katakana, hangul, Thai, Arabic, Hebrew, etc. + // → group consecutive chars into one chunk (no space delimiter in these languages) + if (char.GetUnicodeCategory(ch) == UnicodeCategory.OtherLetter) + cache[i] = CharCategory.OtherLetter; + + // Unicode Ll/Lu/Lt/Lm + digit: latin, greek, cyrillic and their diacritic variants + // → group consecutive chars into one chunk (words in space-delimited languages) + else if (char.IsLetterOrDigit(ch)) + cache[i] = CharCategory.Letter; + + // everything else (whitespace, control, punctuation, symbols) → Other (default) + } + + return cache; + } + + private static CharCategory GetCategory(char ch) => s_charCategoryCache[ch]; + + private static readonly CharCategory[] s_charCategoryCache = BuildCategoryCache(); } } diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 38c0aaa72..5bb50da69 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -29,6 +29,8 @@ public static class GrammarUtility new ExtraGrammar("source.vue", [".vue"], "vue.json"), ]; + private static readonly Dictionary s_cachedRawGrammars = new(); + public static string GetScope(string file, RegistryOptions reg) { var extension = Path.GetExtension(file); @@ -53,6 +55,12 @@ public static string GetScope(string file, RegistryOptions reg) public static IRawGrammar GetGrammar(string scopeName, RegistryOptions reg) { + if (string.IsNullOrEmpty(scopeName)) + return null; + + if (s_cachedRawGrammars.TryGetValue(scopeName, out var cached)) + return cached; + foreach (var grammar in s_extraGrammars) { if (grammar.Scope.Equals(scopeName, StringComparison.OrdinalIgnoreCase)) @@ -62,7 +70,9 @@ public static IRawGrammar GetGrammar(string scopeName, RegistryOptions reg) try { - return GrammarReader.ReadGrammarSync(new StreamReader(asset)); + var raw = GrammarReader.ReadGrammarSync(new StreamReader(asset)); + s_cachedRawGrammars.Add(scopeName, raw); + return raw; } catch { @@ -71,7 +81,9 @@ public static IRawGrammar GetGrammar(string scopeName, RegistryOptions reg) } } - return reg.GetGrammar(scopeName); + var fallback = reg.GetGrammar(scopeName); + s_cachedRawGrammars.Add(scopeName, fallback); + return fallback; } private record ExtraGrammar(string Scope, List Extensions, string File) @@ -88,11 +100,21 @@ public class RegistryOptionsWrapper(ThemeName defaultTheme) : IRegistryOptions public IRawTheme GetTheme(string scopeName) => _backend.GetTheme(scopeName); public IRawTheme GetDefaultTheme() => _backend.GetDefaultTheme(); - public IRawTheme LoadTheme(ThemeName name) => _backend.LoadTheme(name); public ICollection GetInjections(string scopeName) => _backend.GetInjections(scopeName); public IRawGrammar GetGrammar(string scopeName) => GrammarUtility.GetGrammar(scopeName, _backend); public string GetScope(string filename) => GrammarUtility.GetScope(filename, _backend); + public IRawTheme LoadTheme(ThemeName name) + { + if (s_cachedTheme.TryGetValue(name, out var cached)) + return cached; + + var loaded = _backend.LoadTheme(name); + s_cachedTheme.Add(name, loaded); + return loaded; + } + + private static readonly Dictionary s_cachedTheme = new(); private readonly RegistryOptions _backend = new(defaultTheme); } @@ -122,7 +144,7 @@ public static void SetGrammarByFileName(TextMate.Installation installation, stri if (reg.LastScope != scope) { reg.LastScope = scope; - installation.SetGrammar(reg.GetScope(filePath)); + installation.SetGrammar(scope); GC.Collect(); } } diff --git a/src/Models/ThemeOverrides.cs b/src/Models/ThemeOverrides.cs index ccd9f57e8..c80576dc8 100644 --- a/src/Models/ThemeOverrides.cs +++ b/src/Models/ThemeOverrides.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; - using Avalonia.Media; namespace SourceGit.Models diff --git a/src/Models/User.cs b/src/Models/User.cs index 15a45762b..a1b984a04 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -4,35 +4,23 @@ namespace SourceGit.Models { public class User { - public static readonly User Invalid = new User(); + public static readonly User Invalid = new User(string.Empty); public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; - public User() - { - // Only used by User.Invalid - } - public User(string data) { var parts = data.Split('±', 2); if (parts.Length < 2) - parts = [string.Empty, data]; - - Name = parts[0]; - Email = parts[1].TrimStart('<').TrimEnd('>'); - _hash = data.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is User other && Name == other.Name && Email == other.Email; - } - - public override int GetHashCode() - { - return _hash; + { + Email = data; + } + else + { + Name = parts[0]; + Email = parts[1]; + } } public static User FindOrAdd(string data) @@ -46,6 +34,5 @@ public override string ToString() } private static ConcurrentDictionary _caches = new ConcurrentDictionary(); - private readonly int _hash; } } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 843744724..ce18fe4be 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace SourceGit.Models { @@ -27,6 +28,7 @@ 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; @@ -35,13 +37,13 @@ public Watcher(IRepository repo, string fullpath, string gitDir) var combined = new FileSystemWatcher(); combined.Path = fullpath; combined.Filter = "*"; - combined.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; + combined.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; combined.IncludeSubdirectories = true; combined.Created += OnRepositoryChanged; combined.Renamed += OnRepositoryChanged; combined.Changed += OnRepositoryChanged; combined.Deleted += OnRepositoryChanged; - combined.EnableRaisingEvents = true; + combined.EnableRaisingEvents = false; _watchers.Add(combined); } @@ -50,13 +52,13 @@ public Watcher(IRepository repo, string fullpath, string gitDir) var wc = new FileSystemWatcher(); wc.Path = fullpath; wc.Filter = "*"; - wc.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; + wc.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; wc.IncludeSubdirectories = true; wc.Created += OnWorkingCopyChanged; wc.Renamed += OnWorkingCopyChanged; wc.Changed += OnWorkingCopyChanged; wc.Deleted += OnWorkingCopyChanged; - wc.EnableRaisingEvents = true; + wc.EnableRaisingEvents = false; var git = new FileSystemWatcher(); git.Path = gitDir; @@ -67,13 +69,27 @@ 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 IDisposable Lock() @@ -102,6 +118,11 @@ public void MarkStashUpdated() Interlocked.Exchange(ref _updateStashes, 0); } + public void MarkSubmodulesUpdated() + { + Interlocked.Exchange(ref _updateSubmodules, 0); + } + public void Dispose() { foreach (var watcher in _watchers) @@ -281,6 +302,13 @@ private void HandleGitDirFileChanged(string name) { Interlocked.Exchange(ref _updateBranch, DateTime.Now.AddSeconds(.5).ToFileTime()); } + else if (name.StartsWith("reftable/", StringComparison.Ordinal)) + { + var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + Interlocked.Exchange(ref _updateStashes, desired); + } else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal)) { Interlocked.Exchange(ref _updateWC, DateTime.Now.AddSeconds(1).ToFileTime()); @@ -312,26 +340,25 @@ private void HandleWorkingCopyFileChanged(string name, string fullpath) private bool IsInSubmodule(string folder) { + if (string.IsNullOrEmpty(folder) || folder.Equals(_root, StringComparison.Ordinal)) + return false; + if (File.Exists($"{folder}/.git")) return true; - var parent = Path.GetDirectoryName(folder); - if (parent == null || parent.Equals(_root, StringComparison.Ordinal)) - return false; - - return IsInSubmodule(parent); + return IsInSubmodule(Path.GetDirectoryName(folder)); } - private readonly IRepository _repo = null; - private readonly string _root = null; - private List _watchers = []; - private Timer _timer = null; - - private long _lockCount = 0; - private long _updateWC = 0; - private long _updateBranch = 0; - private long _updateSubmodules = 0; - private long _updateStashes = 0; - private long _updateTags = 0; + 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 00eb33a05..27a9415d5 100644 --- a/src/Models/Worktree.cs +++ b/src/Models/Worktree.cs @@ -1,39 +1,12 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; - namespace SourceGit.Models { - public class Worktree : ObservableObject + public class Worktree { public string Branch { get; set; } = string.Empty; public string FullPath { get; set; } = string.Empty; - public string RelativePath { get; set; } = string.Empty; public string Head { get; set; } = string.Empty; + public bool IsBare { get; set; } = false; public bool IsDetached { get; set; } = false; - - public bool IsLocked - { - get => _isLocked; - set => SetProperty(ref _isLocked, value); - } - - public string Name - { - get - { - if (IsDetached) - return $"detached HEAD at {Head.AsSpan(10)}"; - - if (Branch.StartsWith("refs/heads/", StringComparison.Ordinal)) - return Branch.Substring(11); - - if (Branch.StartsWith("refs/remotes/", StringComparison.Ordinal)) - return Branch.Substring(13); - - return Branch; - } - } - - private bool _isLocked = false; + public bool IsLocked { get; set; } = false; } } diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index c0b067e60..a28d88ebe 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -20,6 +20,8 @@ public void SetupApp(AppBuilder builder) public void SetupWindow(Window window) { + window.BorderThickness = new Thickness(0); + if (OS.UseSystemWindowFrame) { window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; @@ -33,6 +35,22 @@ public void SetupWindow(Window window) } } + public string GetDataDir() + { + // AppImage supports portable mode + var appImage = Environment.GetEnvironmentVariable("APPIMAGE"); + if (!string.IsNullOrEmpty(appImage) && File.Exists(appImage)) + { + var portableDir = Path.Combine(Path.GetDirectoryName(appImage)!, "data"); + if (Directory.Exists(portableDir)) + return portableDir; + } + + // Runtime data dir: ~/.sourcegit + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return Path.Combine(home, ".sourcegit"); + } + public string FindGitExecutable() { return FindExecutable("git"); @@ -54,7 +72,6 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.VSCodeInsiders(() => FindExecutable("code-insiders")); finder.VSCodium(() => FindExecutable("codium")); finder.Cursor(() => FindExecutable("cursor")); - finder.Fleet(() => FindJetBrainsFleet(localAppDataDir)); finder.FindJetBrainsFromToolbox(() => Path.Combine(localAppDataDir, "JetBrains/Toolbox")); finder.SublimeText(() => FindExecutable("subl")); finder.Zed(() => @@ -73,7 +90,7 @@ public void OpenBrowser(string url) Process.Start(browser, url.Quoted()); } - public void OpenInFileManager(string path, bool select) + public void OpenInFileManager(string path) { if (Directory.Exists(path)) { @@ -103,7 +120,7 @@ public void OpenTerminal(string workdir, string args) } catch (Exception e) { - App.RaiseException(workdir, $"Failed to start '{OS.ShellOrTerminal}'. Reason: {e.Message}"); + Models.Notification.Send(workdir, $"Failed to start '{OS.ShellOrTerminal}'. Reason: {e.Message}", true); } } @@ -115,7 +132,7 @@ public void OpenWithDefaultEditor(string file) proc.WaitForExit(); if (proc.ExitCode != 0) - App.RaiseException("", $"Failed to open: {file}"); + Models.Notification.Send("", $"Failed to open: {file}", true); proc.Close(); } @@ -135,11 +152,5 @@ private string FindExecutable(string filename) var local = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", filename); return File.Exists(local) ? local : string.Empty; } - - private string FindJetBrainsFleet(string localAppDataDir) - { - var path = Path.Combine(localAppDataDir, "JetBrains/Toolbox/apps/fleet/bin/Fleet"); - return File.Exists(path) ? path : FindExecutable("fleet"); - } } } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 48d276496..54a2a8a72 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using Avalonia; @@ -42,6 +44,14 @@ public void SetupWindow(Window window) { window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome; window.ExtendClientAreaToDecorationsHint = true; + window.BorderThickness = new Thickness(0); + } + + public string GetDataDir() + { + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "SourceGit"); } public string FindGitExecutable() @@ -73,7 +83,6 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.VSCodeInsiders(() => "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code"); finder.VSCodium(() => "/Applications/VSCodium.app/Contents/Resources/app/bin/codium"); finder.Cursor(() => "/Applications/Cursor.app/Contents/Resources/app/bin/cursor"); - finder.Fleet(() => Path.Combine(home, "Applications/Fleet.app/Contents/MacOS/Fleet")); finder.FindJetBrainsFromToolbox(() => Path.Combine(home, "Library/Application Support/JetBrains/Toolbox")); finder.SublimeText(() => "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"); finder.Zed(() => File.Exists("/usr/local/bin/zed") ? "/usr/local/bin/zed" : "/Applications/Zed.app/Contents/MacOS/cli"); @@ -85,7 +94,7 @@ public void OpenBrowser(string url) Process.Start("open", url); } - public void OpenInFileManager(string path, bool select) + public void OpenInFileManager(string path) { if (Directory.Exists(path)) Process.Start("open", path.Quoted()); @@ -105,4 +114,175 @@ public void OpenWithDefaultEditor(string file) Process.Start("open", file.Quoted()); } } + + [SupportedOSPlatform("macOS")] + public static class MacOSUtilities + { + [StructLayout(LayoutKind.Sequential)] + public struct CGPoint + { + public double X; + public double Y; + + public CGPoint(double x, double y) + { + X = x; + Y = y; + } + + public override int GetHashCode() + { + return HashCode.Combine(X, Y); + } + + public override bool Equals([NotNullWhen(true)] object obj) + { + if (obj is not CGPoint point) + return false; + + return X == point.X && Y == point.Y; + } + + public static bool operator ==(CGPoint left, CGPoint right) => left.Equals(right); + public static bool operator !=(CGPoint left, CGPoint right) => !left.Equals(right); + } + + [StructLayout(LayoutKind.Sequential)] + public struct CGSize + { + public double Width; + public double Height; + + public override int GetHashCode() + { + return HashCode.Combine(Width, Height); + } + + public override bool Equals([NotNullWhen(true)] object obj) + { + if (obj is not CGSize size) + return false; + + return Width == size.Width && Height == size.Height; + } + + public static bool operator ==(CGSize left, CGSize right) => left.Equals(right); + public static bool operator !=(CGSize left, CGSize right) => !left.Equals(right); + } + + [StructLayout(LayoutKind.Sequential)] + public struct CGRect + { + public CGPoint Origin; + public CGSize Size; + + public override int GetHashCode() + { + return HashCode.Combine(Origin, Size); + } + + public override bool Equals([NotNullWhen(true)] object obj) + { + if (obj is not CGRect rect) + return false; + + return Origin == rect.Origin && Size == rect.Size; + } + + public static bool operator ==(CGRect left, CGRect right) => left.Equals(right); + public static bool operator !=(CGRect left, CGRect right) => !left.Equals(right); + } + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_getClass")] + public static extern IntPtr objc_getClass(string name); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "sel_registerName")] + public static extern IntPtr sel_registerName(string name); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] + public static extern IntPtr objc_msgSend_IntPtr(IntPtr receiver, IntPtr selector); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] + public static extern IntPtr objc_msgSend_IntPtr_Int(IntPtr receiver, IntPtr selector, int arg); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] + public static extern void objc_msgSend_Void_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] + public static extern void objc_msgSend_Void_Point(IntPtr receiver, IntPtr selector, CGPoint arg); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend_stret")] + public static extern void objc_msgSendStrect_Rect(out CGRect rect, IntPtr receiver, IntPtr selector); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] + public static extern CGRect objc_msgSend_Rect(IntPtr receiver, IntPtr selector); + + private static readonly IntPtr s_selStandardWindowButton = sel_registerName("standardWindowButton:"); + private static readonly IntPtr s_selFrame = sel_registerName("frame"); + private static readonly IntPtr s_selSetFrameOrigin = sel_registerName("setFrameOrigin:"); + + public static void AdjustTrafficLightsForThickTitleBar(Window window) + { + IntPtr nsWindow = window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; + if (nsWindow == IntPtr.Zero) + return; + + IntPtr nsCloseBtn = objc_msgSend_IntPtr_Int(nsWindow, s_selStandardWindowButton, 0); + IntPtr nsMinBtn = objc_msgSend_IntPtr_Int(nsWindow, s_selStandardWindowButton, 1); + IntPtr nsZoomBtn = objc_msgSend_IntPtr_Int(nsWindow, s_selStandardWindowButton, 2); + if (nsCloseBtn == IntPtr.Zero || nsMinBtn == IntPtr.Zero || nsZoomBtn == IntPtr.Zero) + return; + + // For Intel CPU, we need to use `objc_msgSend_stret` to get the `CGRect` struct, while for Apple Silicon, we can directly use `objc_msgSend`. + if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + objc_msgSendStrect_Rect(out var frame, nsCloseBtn, s_selFrame); + if (Math.Abs(frame.Origin.X - 14) <= 0.5 || Math.Abs(frame.Origin.Y - 2) <= 0.5) + return; + } + else + { + CGRect frame = objc_msgSend_Rect(nsCloseBtn, s_selFrame); + if (Math.Abs(frame.Origin.X - 14) <= 0.5 || Math.Abs(frame.Origin.Y - 2) <= 0.5) + return; + } + + objc_msgSend_Void_Point(nsCloseBtn, s_selSetFrameOrigin, new(14, 2)); + objc_msgSend_Void_Point(nsMinBtn, s_selSetFrameOrigin, new(14 + 20, 2)); + objc_msgSend_Void_Point(nsZoomBtn, s_selSetFrameOrigin, new(14 + 40, 2)); + } + + public static void HideSelf() + { + IntPtr nsApplicationClass = objc_getClass("NSApplication"); + IntPtr nsSharedApplicationSelector = sel_registerName("sharedApplication"); + IntPtr nsApp = objc_msgSend_IntPtr(nsApplicationClass, nsSharedApplicationSelector); + IntPtr nsMethodSelector = sel_registerName("hide:"); + IntPtr nsDelegateSelector = sel_registerName("delegate"); + IntPtr nsDelegate = objc_msgSend_IntPtr(nsApp, nsDelegateSelector); + objc_msgSend_Void_IntPtr(nsApp, nsMethodSelector, nsDelegate); + } + + public static void HideOtherApplications() + { + IntPtr nsApplicationClass = objc_getClass("NSApplication"); + IntPtr nsSharedApplicationSelector = sel_registerName("sharedApplication"); + IntPtr nsApp = objc_msgSend_IntPtr(nsApplicationClass, nsSharedApplicationSelector); + IntPtr nsMethodSelector = sel_registerName("hideOtherApplications:"); + IntPtr nsDelegateSelector = sel_registerName("delegate"); + IntPtr nsDelegate = objc_msgSend_IntPtr(nsApp, nsDelegateSelector); + objc_msgSend_Void_IntPtr(nsApp, nsMethodSelector, nsDelegate); + } + + public static void ShowAllApplications() + { + IntPtr nsApplicationClass = objc_getClass("NSApplication"); + IntPtr nsSharedApplicationSelector = sel_registerName("sharedApplication"); + IntPtr nsApp = objc_msgSend_IntPtr(nsApplicationClass, nsSharedApplicationSelector); + IntPtr nsMethodSelector = sel_registerName("unhideAllApplications:"); + IntPtr nsDelegateSelector = sel_registerName("delegate"); + IntPtr nsDelegate = objc_msgSend_IntPtr(nsApp, nsDelegateSelector); + objc_msgSend_Void_IntPtr(nsApp, nsMethodSelector, nsDelegate); + } + } } diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 4da62b5ff..ceaa69c84 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; - +using System.Threading; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; @@ -17,12 +19,13 @@ public interface IBackend void SetupApp(AppBuilder builder); void SetupWindow(Window window); + string GetDataDir(); string FindGitExecutable(); string FindTerminal(Models.ShellOrTerminal shell); List FindExternalTools(); void OpenTerminal(string workdir, string args); - void OpenInFileManager(string path, bool select); + void OpenInFileManager(string path); void OpenBrowser(string url); void OpenWithDefaultEditor(string file); } @@ -58,6 +61,12 @@ public static Version GitVersion private set; } = new Version(0, 0, 0); + public static Models.GitFlowVersion GitFlowVersion + { + get; + private set; + } = Models.GitFlowVersion.None; + public static string CredentialHelper { get; @@ -115,51 +124,27 @@ public static bool UseSystemWindowFrame static OS() { if (OperatingSystem.IsWindows()) - { _backend = new Windows(); - } else if (OperatingSystem.IsMacOS()) - { _backend = new MacOS(); - } else if (OperatingSystem.IsLinux()) - { _backend = new Linux(); - } else - { throw new PlatformNotSupportedException(); - } - } - - public static void SetupApp(AppBuilder builder) - { - _backend.SetupApp(builder); } public static void SetupDataDir() { - if (OperatingSystem.IsWindows()) - { - var execFile = Process.GetCurrentProcess().MainModule!.FileName; - var portableDir = Path.Combine(Path.GetDirectoryName(execFile)!, "data"); - if (Directory.Exists(portableDir)) - { - DataDir = portableDir; - return; - } - } - - var osAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - if (string.IsNullOrEmpty(osAppDataDir)) - DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit"); - else - DataDir = Path.Combine(osAppDataDir, "SourceGit"); - + DataDir = _backend.GetDataDir(); if (!Directory.Exists(DataDir)) Directory.CreateDirectory(DataDir); } + public static void SetupApp(AppBuilder builder) + { + _backend.SetupApp(builder); + } + public static void SetupExternalTools() { ExternalTools = _backend.FindExternalTools(); @@ -170,6 +155,35 @@ public static void SetupForWindow(Window window) _backend.SetupWindow(window); } + public static void LogException(Exception ex) + { + if (ex == null) + return; + + var crashDir = Path.Combine(DataDir, "crashes"); + if (!Directory.Exists(crashDir)) + Directory.CreateDirectory(crashDir); + + var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + var file = Path.Combine(crashDir, $"{time}.log"); + using var writer = new StreamWriter(file); + writer.WriteLine($"Crash::: {ex.GetType().FullName}: {ex.Message}"); + writer.WriteLine(); + writer.WriteLine("----------------------------"); + writer.WriteLine($"Version: {Assembly.GetExecutingAssembly().GetName().Version}"); + writer.WriteLine($"OS: {Environment.OSVersion}"); + writer.WriteLine($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}"); + writer.WriteLine($"Source: {ex.Source}"); + writer.WriteLine($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}"); + writer.WriteLine($"App Start Time: {Process.GetCurrentProcess().StartTime}"); + writer.WriteLine($"Exception Time: {DateTime.Now}"); + writer.WriteLine($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB"); + writer.WriteLine("----------------------------"); + writer.WriteLine(); + writer.WriteLine(ex); + writer.Flush(); + } + public static string FindGitExecutable() { return _backend.FindGitExecutable(); @@ -182,11 +196,7 @@ public static bool TestShellOrTerminal(Models.ShellOrTerminal shell) public static void SetShellOrTerminal(Models.ShellOrTerminal shell) { - if (shell == null) - ShellOrTerminal = string.Empty; - else - ShellOrTerminal = _backend.FindTerminal(shell); - + ShellOrTerminal = shell != null ? _backend.FindTerminal(shell) : string.Empty; ShellOrTerminalArgs = shell.Args; } @@ -198,7 +208,6 @@ public static Models.DiffMergeTool GetDiffMergeTool(bool onlyDiff) if (ExternalMergerType != 0 && (string.IsNullOrEmpty(ExternalMergerExecFile) || !File.Exists(ExternalMergerExecFile))) return null; - var tool = Models.ExternalMerger.Supported[ExternalMergerType]; return new Models.DiffMergeTool(ExternalMergerExecFile, onlyDiff ? ExternalDiffArgs : ExternalMergeArgs); } @@ -226,20 +235,37 @@ public static void AutoSelectExternalMergeToolExecFile() } } - public static void OpenInFileManager(string path, bool select = false) + public static void OpenInFileManager(string path) { - _backend.OpenInFileManager(path, select); + _backend.OpenInFileManager(path); } public static void OpenBrowser(string url) { + if (!IsSafeBrowserTarget(url)) + { + Models.Notification.Send(null, $"Blocked unsafe URL: {url}", true); + return; + } + _backend.OpenBrowser(url); } + private static bool IsSafeBrowserTarget(string url) + { + return Uri.IsWellFormedUriString(url, UriKind.Absolute) && + Uri.TryCreate(url, UriKind.Absolute, out var uri) && + ( + uri.Scheme == Uri.UriSchemeHttp || + uri.Scheme == Uri.UriSchemeHttps || + uri.Scheme == Uri.UriSchemeFtp + ); + } + 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."); + Models.Notification.Send(workdir, "Terminal is not specified! Please confirm that the correct shell/terminal has been configured.", true); else _backend.OpenTerminal(workdir, ShellOrTerminalArgs); } @@ -277,6 +303,7 @@ private static void UpdateGitVersion() { GitVersionString = string.Empty; GitVersion = new Version(0, 0, 0); + GitFlowVersion = Models.GitFlowVersion.None; return; } @@ -308,6 +335,42 @@ private static void UpdateGitVersion() GitVersion = new Version(major, minor, build); GitVersionString = GitVersionString.Substring(11).Trim(); } + + // Update git flow version in background to avoid blocking the UI + Task.Run(UpdateGitFlowVersion); + } + } + catch + { + // Ignore errors + } + } + + private static void UpdateGitFlowVersion() + { + var start = new ProcessStartInfo(); + start.FileName = _gitExecutable; + start.Arguments = "flow version"; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + + GitFlowVersion = Models.GitFlowVersion.None; + + try + { + using var proc = Process.Start(start)!; + var rs = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + if (proc.ExitCode == 0 && !string.IsNullOrWhiteSpace(rs)) + { + if (rs.Contains("git-flow-next", StringComparison.Ordinal)) + GitFlowVersion = Models.GitFlowVersion.Next; + else + GitFlowVersion = Models.GitFlowVersion.Legacy; } } catch diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 756eeecac..2ec98ca40 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -17,27 +17,6 @@ namespace SourceGit.Native [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { - [StructLayout(LayoutKind.Sequential)] - internal struct RECT - { - public int left; - public int top; - public int right; - public int bottom; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct MARGINS - { - public int cxLeftWidth; - public int cxRightWidth; - public int cyTopHeight; - public int cyBottomHeight; - } - - [DllImport("dwmapi.dll")] - private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); - [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs); @@ -50,17 +29,9 @@ internal struct MARGINS [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags); - [DllImport("user32.dll")] - private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); - public void SetupApp(AppBuilder builder) { - // Fix drop shadow issue on Windows 10 - if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) - { - Window.WindowStateProperty.Changed.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); - Control.LoadedEvent.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); - } + // Do nothing for now. } public void SetupWindow(Window window) @@ -68,50 +39,19 @@ public void SetupWindow(Window window) window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; window.ExtendClientAreaToDecorationsHint = true; window.BorderThickness = new Thickness(1); + window.Padding = new Thickness(0); + } - Win32Properties.AddWndProcHookCallback(window, (IntPtr hWnd, uint msg, IntPtr _, IntPtr lParam, ref bool handled) => - { - // Custom WM_NCHITTEST only used to limit the resize border to 4 * window.RenderScaling pixels. - if (msg == 0x0084 && window.WindowState == WindowState.Normal) - { - var p = IntPtrToPixelPoint(lParam); - GetWindowRect(hWnd, out var rcWindow); - - var borderThickness = (int)(4 * window.RenderScaling); - int y = 1; - int x = 1; - if (p.X >= rcWindow.left && p.X < rcWindow.left + borderThickness) - x = 0; - else if (p.X < rcWindow.right && p.X >= rcWindow.right - borderThickness) - x = 2; - - if (p.Y >= rcWindow.top && p.Y < rcWindow.top + borderThickness) - y = 0; - else if (p.Y < rcWindow.bottom && p.Y >= rcWindow.bottom - borderThickness) - y = 2; - - // 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 - 5 => 11, // HTRIGHT - 6 => 16, // HTBOTTOMLEFT - 7 => 15, // HTBOTTOM - _ => 17, // HTBOTTOMRIGHT - }; - } - - return IntPtr.Zero; - }); + public string GetDataDir() + { + var execFile = Environment.ProcessPath; + var portableDir = Path.Combine(Path.GetDirectoryName(execFile)!, "data"); + if (Directory.Exists(portableDir)) + return portableDir; + + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "SourceGit"); } public string FindGitExecutable() @@ -187,18 +127,18 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.VSCode(FindVSCode); finder.VSCodeInsiders(FindVSCodeInsiders); finder.VSCodium(FindVSCodium); + FindVisualStudio(finder); 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; } public void OpenBrowser(string url) { - var info = new ProcessStartInfo("cmd", $"""/c start "" {url.Quoted()}"""); + var info = new ProcessStartInfo(url); + info.UseShellExecute = true; info.CreateNoWindow = true; Process.Start(info); } @@ -211,7 +151,7 @@ public void OpenTerminal(string workdir, string args) if (!File.Exists(terminal)) { - App.RaiseException(workdir, "Terminal is not specified! Please confirm that the correct shell/terminal has been configured."); + Models.Notification.Send(workdir, "Terminal is not specified! Please confirm that the correct shell/terminal has been configured.", true); return; } @@ -222,32 +162,30 @@ public void OpenTerminal(string workdir, string args) Process.Start(startInfo); } - public void OpenInFileManager(string path, bool select) + public void OpenInFileManager(string path) { - string fullpath; if (File.Exists(path)) { - fullpath = new FileInfo(path).FullName; - select = true; - } - else - { - fullpath = new DirectoryInfo(path!).FullName; - fullpath += Path.DirectorySeparatorChar; - } + var pidl = ILCreateFromPathW(new FileInfo(path).FullName); - if (select) - { - OpenFolderAndSelectFile(fullpath); - } - else - { - Process.Start(new ProcessStartInfo(fullpath) + try + { + SHOpenFolderAndSelectItems(pidl, 0, 0, 0); + } + finally { - UseShellExecute = true, - CreateNoWindow = true, - }); + ILFree(pidl); + } + + return; } + + var dir = new DirectoryInfo(path).FullName + Path.DirectorySeparatorChar; + Process.Start(new ProcessStartInfo(dir) + { + UseShellExecute = true, + CreateNoWindow = true, + }); } public void OpenWithDefaultEditor(string file) @@ -258,26 +196,27 @@ public void OpenWithDefaultEditor(string file) Process.Start(start); } - private void FixWindowFrameOnWin10(Window w) + #region HELPER_METHODS + private List GenerateVSProjectLaunchOptions(string path) { - // Schedule the DWM frame extension to run in the next render frame - // to ensure proper timing with the window initialization sequence - Dispatcher.UIThread.Post(() => - { - var platformHandle = w.TryGetPlatformHandle(); - if (platformHandle == null) - return; - - var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 }; - DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); - }, DispatcherPriority.Render); - } + var root = new DirectoryInfo(path); + if (!root.Exists) + return null; - private PixelPoint IntPtrToPixelPoint(IntPtr param) - { - var v = IntPtr.Size == 4 ? param.ToInt32() : (int)(param.ToInt64() & 0xFFFFFFFF); - return new PixelPoint((short)(v & 0xffff), (short)(v >> 16)); + var options = new List(); + var prefixLen = root.FullName.Length; + root.WalkFiles(f => + { + if (f.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) || + f.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase)) + { + var display = f.Substring(prefixLen).TrimStart(Path.DirectorySeparatorChar); + options.Add(new(display, f.Quoted())); + } + }); + return options; } + #endregion #region EXTERNAL_EDITOR_FINDER private string FindVSCode() @@ -402,7 +341,7 @@ private void FindVisualStudio(Models.ExternalToolsFinder finder) { var exec = instance.ProductPath; var icon = instance.IsPrerelease ? "vs-preview" : "vs"; - finder.TryAdd(instance.DisplayName, icon, () => exec, GenerateCommandlineArgsForVisualStudio); + finder.TryAdd(instance.DisplayName, icon, () => exec, GenerateVSProjectLaunchOptions, false); } } } @@ -430,54 +369,48 @@ private string FindZed() return string.Empty; } #endregion + } - private void OpenFolderAndSelectFile(string folderPath) + [SupportedOSPlatform("windows")] + public static class Win64Utilities + { + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS { - var pidl = ILCreateFromPathW(folderPath); - - try - { - SHOpenFolderAndSelectItems(pidl, 0, 0, 0); - } - finally - { - ILFree(pidl); - } + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; } - private string GenerateCommandlineArgsForVisualStudio(string path) + [DllImport("dwmapi.dll")] + private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + + public static void FixWindowFrame(Window w) { - if (Directory.Exists(path)) + if (w.WindowState == WindowState.Maximized) { - var sln = FindVSSolutionFile(new DirectoryInfo(path), 4); - return string.IsNullOrEmpty(sln) ? path.Quoted() : sln.Quoted(); + w.BorderThickness = new Thickness(0); + w.Padding = new Thickness(8, 6, 8, 8); } - - return path.Quoted(); - } - - private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth) - { - var files = dir.GetFiles(); - foreach (var f in files) + else { - if (f.Name.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase) || - f.Name.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) - return f.FullName; + w.BorderThickness = new Thickness(1); + w.Padding = new Thickness(0); } - if (leftDepth <= 0) - return null; + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) + return; - var subDirs = dir.GetDirectories(); - foreach (var subDir in subDirs) + Dispatcher.UIThread.Post(() => { - var first = FindVSSolutionFile(subDir, leftDepth - 1); - if (!string.IsNullOrEmpty(first)) - return first; - } + var platformHandle = w.TryGetPlatformHandle(); + if (platformHandle == null) + return; - return null; + var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 }; + DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); + }, DispatcherPriority.Render); } } } diff --git a/src/Resources/Fonts/JetBrainsMono-Bold.ttf b/src/Resources/Fonts/JetBrainsMono-Bold.ttf deleted file mode 100644 index 8c93043de..000000000 Binary files a/src/Resources/Fonts/JetBrainsMono-Bold.ttf and /dev/null differ diff --git a/src/Resources/Fonts/JetBrainsMono-Italic.ttf b/src/Resources/Fonts/JetBrainsMono-Italic.ttf deleted file mode 100644 index ccc9d6a5b..000000000 Binary files a/src/Resources/Fonts/JetBrainsMono-Italic.ttf and /dev/null differ diff --git a/src/Resources/Fonts/JetBrainsMono-Regular.ttf b/src/Resources/Fonts/JetBrainsMono-Regular.ttf deleted file mode 100644 index dff66cc50..000000000 Binary files a/src/Resources/Fonts/JetBrainsMono-Regular.ttf and /dev/null differ diff --git a/src/Resources/Fonts/JetBrainsMonoNL-Bold.ttf b/src/Resources/Fonts/JetBrainsMonoNL-Bold.ttf new file mode 100644 index 000000000..f78f84fb4 Binary files /dev/null and b/src/Resources/Fonts/JetBrainsMonoNL-Bold.ttf differ diff --git a/src/Resources/Fonts/JetBrainsMonoNL-Italic.ttf b/src/Resources/Fonts/JetBrainsMonoNL-Italic.ttf new file mode 100644 index 000000000..4e9c3802d Binary files /dev/null and b/src/Resources/Fonts/JetBrainsMonoNL-Italic.ttf differ diff --git a/src/Resources/Fonts/JetBrainsMonoNL-Regular.ttf b/src/Resources/Fonts/JetBrainsMonoNL-Regular.ttf new file mode 100644 index 000000000..70d2ec9e2 Binary files /dev/null and b/src/Resources/Fonts/JetBrainsMonoNL-Regular.ttf differ diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 4fd176dfa..6852b6a51 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -1,15 +1,18 @@ M41 512c0-128 46-241 138-333C271 87 384 41 512 41s241 46 333 138c92 92 138 205 138 333s-46 241-138 333c-92 92-205 138-333 138s-241-46-333-138C87 753 41 640 41 512zm87 0c0 108 36 195 113 271s164 113 271 113c108 0 195-36 271-113s113-164 113-271-36-195-113-271c-77-77-164-113-271-113-108 0-195 36-271 113C164 317 128 404 128 512zm256 148V292l195 113L768 512l-195 113-195 113v-77zm148-113-61 36V440l61 36 61 36-61 36z M304 464a128 128 0 01128-128c71 0 128 57 128 128v224a32 32 0 01-64 0V592h-128v95a32 32 0 01-64 0v-224zm64 1v64h128v-64a64 64 0 00-64-64c-35 0-64 29-64 64zM688 337c18 0 32 14 32 32v319a32 32 0 01-32 32c-18 0-32-14-32-32v-319a32 32 0 0132-32zM84 911l60-143A446 446 0 0164 512C64 265 265 64 512 64s448 201 448 448-201 448-448 448c-54 0-105-9-153-27l-242 22a32 32 0 01-32-44zm133-150-53 126 203-18 13 5c41 15 85 23 131 23 212 0 384-172 384-384S724 128 512 128 128 300 128 512c0 82 26 157 69 220l20 29z - M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z + M233 163h560c11 0 20-9 20-20V63h-226c-6-36-37-63-74-63s-68 27-74 63h-226v80c0 11 9 20 20 20zM883 63h-10v112c0 33-27 60-60 60h-600c-33 0-60-27-60-60V63h-10c-44 0-80 36-80 80v801c0 44 36 80 80 80h740c44 0 80-36 80-80V143c0-44-36-80-80-80zm-720 469c0-24 20-44 44-44h62v-72c0-24 20-44 44-44s44 20 44 44v72h62c24 0 44 20 44 44s-20 44-44 44h-62v72c0 24-20 44-44 44s-44-20-44-44v-72h-62c-24 0-44-20-44-44zm700 307c0 24-20 44-44 44h-612c-24 0-44-20-44-44s20-44 44-44h612c24 0 44 20 44 44zm0-191c0 24-20 44-44 44h-232c-24 0-44-20-44-44s20-44 44-44h232c24 0 44 20 44 44zm0-191c0 24-20 44-44 44h-162c-24 0-44-20-44-44s20-44 44-44h162c24 0 44 20 44 44z + M366 146l293 0 0-73-293 0 0 73zm658 366 0 274q0 38-27 65t-65 27l-841 0q-38 0-65-27t-27-65l0-274 384 0 0 91q0 15 11 26t26 11l183 0q15 0 26-11t11-26l0-91 384 0zm-439 0 0 73-146 0 0-73 146 0zm439-274 0 219-1024 0 0-219q0-38 27-65t65-27l201 0 0-91q0-23 16-39t39-16l329 0q23 0 39 16t16 39l0 91 201 0q38 0 65 27t27 65z + M567 192C385 192 238 335 238 512H128l142 138 3 5L421 512H311c0-138 114-249 256-249s256 111 256 249c0 138-114 249-256 249a258 258 0 01-181-73l-52 51A332 332 0 00567 832C749 832 896 689 896 512S749 192 567 192zm-37 178v178l155 90 28-46-129-74v-148H530z M851 755q0 23-16 39l-78 78q-16 16-39 16t-39-16l-168-168-168 168q-16 16-39 16t-39-16l-78-78q-16-16-16-39t16-39l168-168-168-168q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 168 168-168q16-16 39-16t39 16l78 78q16 16 16 39t-16 39l-168 168 168 168q16 16 16 39z M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z M128 384a43 43 0 0043-43V213a43 43 0 0143-43h128a43 43 0 000-85H213a128 128 0 00-128 128v128a43 43 0 0043 43zm213 469H213a43 43 0 01-43-43v-128a43 43 0 00-85 0v128a128 128 0 00128 128h128a43 43 0 000-85zm384-299a43 43 0 000-85h-49A171 171 0 00555 347V299a43 43 0 00-85 0v49A171 171 0 00347 469H299a43 43 0 000 85h49A171 171 0 00469 677V725a43 43 0 0085 0v-49A171 171 0 00677 555zm-213 43a85 85 0 1185-85 85 85 0 01-85 85zm384 43a43 43 0 00-43 43v128a43 43 0 01-43 43h-128a43 43 0 000 85h128a128 128 0 00128-128v-128a43 43 0 00-43-43zM811 85h-128a43 43 0 000 85h128a43 43 0 0143 43v128a43 43 0 0085 0V213a128 128 0 00-128-128z M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z - M832 64H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V96c0-18-14-32-32-32zM736 596 624 502 506 596V131h230v318z + M171 85h683a43 43 0 0143 43v822a21 21 0 01-30 19L512 812l-354 158A21 21 0 01128 950V128a43 43 0 0143-43zm341 491 125 66-24-140 101-99-140-20L512 256l-63 127-140 20 101 99-24 140L512 576z M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z M757 226a143 143 0 00-55 276 96 96 0 01-88 59h-191a187 187 0 00-96 27V312a143 143 0 10-96 0v399a143 143 0 10103 2 96 96 0 0188-59h191a191 191 0 00187-151 143 143 0 00-43-279zM280 130a48 48 0 110 96 48 48 0 010-96zm0 764a48 48 0 110-96 48 48 0 010 96zM757 417a48 48 0 110-96 48 48 0 010 96z M896 128h-64V64c0-35-29-64-64-64s-64 29-64 64v64h-64c-35 0-64 29-64 64s29 64 64 64h64v64c0 35 29 64 64 64s64-29 64-64V256h64c35 0 64-29 64-64s-29-64-64-64zm-204 307C673 481 628 512 576 512H448c-47 0-90 13-128 35V372C394 346 448 275 448 192c0-106-86-192-192-192S64 86 64 192c0 83 54 154 128 180v280c-74 26-128 97-128 180c0 106 86 192 192 192s192-86 192-192c0-67-34-125-84-159c22-20 52-33 84-33h128c122 0 223-85 249-199c-19 4-37 7-57 7c-26 0-51-5-76-13zM256 128c35 0 64 29 64 64s-29 64-64 64s-64-29-64-64s29-64 64-64zm0 768c-35 0-64-29-64-64s29-64 64-64s64 29 64 64s-29 64-64 64z + M325 171a100 100 0 00-96 71l-93 301-66 213a43 43 0 1082 25L209 597h232l56 183a43 43 0 0082-25l-66-213-92-301A100 100 0 00325 171zm14 96L415 512H235l76-245a15 15 0 0129 0zm382 144C751 392 792 384 843 384v0c59 0 103 15 131 44 25 26 37 63 37 111v225a41 41 0 11-82 0v-22a156 156 0 01-57 46c-26 12-56 18-91 18-42 0-74-11-98-32-25-22-37-49-37-82 0-45 17-80 53-104 33-23 78-36 137-37l87-2v-16c0-53-29-78-86-78-25 0-44 4-59 13-6 4-11 8-16 13-15 16-34 32-56 31-26-2-46-27-33-50a127 127 0 0149-50zm200 221v-20l-81 2c-70 2-105 26-105 74a41 41 0 0018 35c14 10 30 15 47 15 34 0 63-10 86-29 23-20 36-46 36-77z M512 597m-1 0a1 1 0 103 0a1 1 0 10-3 0ZM810 393 732 315 448 600 293 444 214 522l156 156 78 78 362-362z M512 32C246 32 32 250 32 512s218 480 480 480 480-218 480-480S774 32 512 32zm269 381L496 698c-26 26-61 26-83 0L243 528c-26-26-26-61 0-83s61-26 83 0l128 128 240-240c26-26 61-26 83 0 26 19 26 54 3 80z M747 467c29 0 56 4 82 12v-363c0-47-38-84-84-84H125c-47 0-84 38-84 84v707c0 47 38 84 84 84h375a287 287 0 01-43-152c0-160 129-289 289-289zm-531-250h438c19 0 34 15 34 34s-15 34-34 34H216c-19 0-34-15-34-34s15-34 34-34zm0 179h263c19 0 34 15 34 34s-15 34-34 34H216c-19 0-34-15-34-34s15-34 34-34zm131 247h-131c-19 0-34-15-34-34s15-34 34-34h131c19 0 34 15 34 34s-15 34-34 34zM747 521c-130 0-236 106-236 236S617 992 747 992s236-106 236-236S877 521 747 521zm11 386v-65h-130c-12 0-22-10-22-22s10-22 22-22h260l-130 108zm108-192H606l130-108v65h130c12 0 22 10 22 22s-10 22-22 22z @@ -20,15 +23,18 @@ 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 + M277 708h469a43 43 0 110 85h-469a43 43 0 110-85zM331 85h362c34 0 63 0 86 2 24 2 46 6 67 17a171 171 0 0175 75c11 21 15 43 17 67C939 268 939 297 939 331v362c0 34 0 63-2 86-2 24-6 46-17 67a171 171 0 01-75 75c-21 11-43 15-67 17-23 2-51 2-86 2H331c-34 0-63 0-86-2-24-2-46-6-67-17a171 171 0 01-75-75c-11-21-15-43-17-67C85 756 85 727 85 693V331c0-34 0-63 2-86 2-24 6-46 17-67a171 171 0 0175-75c21-11 43-15 67-17C268 85 297 85 331 85zM252 172c-19 1-28 4-35 8a85 85 0 00-37 37c-3 7-6 16-8 35C171 271 171 296 171 333v358c0 37 0 61 2 81 1 19 4 28 8 35a85 85 0 0037 37c7 3 16 6 35 8C271 853 296 853 333 853h358c37 0 61 0 81-2 19-1 28-4 35-8a85 85 0 0037-37c3-7 6-16 8-35 2-19 2-44 2-81V333c0-37 0-61-2-81-1-19-4-28-8-35a85 85 0 00-37-37c-7-3-16-6-35-8C753 171 728 171 691 171H333c-37 0-61 0-81 2z 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 + M64 128h896v64H64V128zm832 128h-64v512h64V256zm-175 281a151 151 0 00-23-50 112 112 0 00-39-34 117 117 0 00-55-12c-13 0-24 2-35 5a113 113 0 00-29 13 110 110 0 00-23 20l-11 17V293H448V768h57v-37l8 11c6 7 12 12 19 17 7 5 16 9 25 12 10 3 20 4 32 4 23 0 43-5 60-14 17-9 32-22 44-38 12-16 21-35 26-56 6-22 9-45 9-70a240 240 0 00-8-62zM627 498c10 5 18 11 25 20 7 9 12 20 16 33 3 11 5 24 6 38l-0 8c0 21-2 40-6 56a121 121 0 01-18 39c-7 10-17 18-27 23-21 10-49 11-68 1a81 81 0 01-25-19 81 81 0 01-14-23s-11-29-11-61c0-32 11-64 11-64 4-11 9-20 14-28 7-9 16-17 27-22 11-6 23-8 38-8 12 0 23 2 33 7zM960 832H64v64h896v-64zM180 640l-47 130H64l2-5 156-454h60l159 458H372L322 640H180zm72-229h-1l-58 176h118l-58-176z 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 + M853 137h-102V102h-68v34H341V102h-68v34h-102C133 137 102 167 102 205v68h819V205c0-38-31-68-68-68zM102 341v512C102 891 133 922 171 922h683C891 922 922 891 922 853v-512H102zM444 819h-68V512H307v-68h137V819zm273-273a102 102 0 01-30 72l-97 97 0 0a34 34 0 00-10 24v12H717V819H512v-80a102 102 0 0130-72l97-97a34 34 0 0010-24h0C649 527 633 512 614 512s-34 15-34 34H512c0-57 46-102 102-102s102 46 102 102h-0 0z 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 M854 307 611 73c-6-6-14-9-22-9H296c-4 0-8 4-8 8v56c0 4 4 8 8 8h277l219 211V824c0 4 4 8 8 8h56c4 0 8-4 8-8V330c0-9-4-17-10-23zM553 201c-6-6-14-9-23-9H192c-18 0-32 14-32 32v704c0 18 14 32 32 32h512c18 0 32-14 32-32V397c0-9-3-17-9-23L553 201zM568 753c0 4-3 7-8 7h-225c-4 0-8-3-8-7v-42c0-4 3-7 8-7h225c4 0 8 3 8 7v42zm0-220c0 4-3 7-8 7H476v85c0 4-3 7-7 7h-42c-4 0-7-3-7-7V540h-85c-4 0-8-3-8-7v-42c0-4 3-7 8-7H420v-85c0-4 3-7 7-7h42c4 0 7 3 7 7V484h85c4 0 8 3 8 7v42z @@ -47,6 +53,7 @@ M959 320H960v640A64 64 0 01896 1024H192A64 64 0 01128 960V64A64 64 0 01192 0H640v321h320L959 320zM320 544c0 17 14 32 32 32h384A32 32 0 00768 544c0-17-14-32-32-32H352A32 32 0 00320 544zm0 128c0 17 14 32 32 32h384a32 32 0 0032-32c0-17-14-32-32-32H352a32 32 0 00-32 32zm0 128c0 17 14 32 32 32h384a32 32 0 0032-32c0-17-14-32-32-32H352a32 32 0 00-32 32z M683 85l213 213v598a42 42 0 01-42 42H170A43 43 0 01128 896V128C128 104 147 85 170 85H683zm-213 384H341v85h128v128h85v-128h128v-85h-128V341h-85v128z M949 727l-217-231a33 33 0 00-48 0 33 33 0 000 48l157 172H389a35 35 0 00-35 35c0 19 16 34 35 34h452l-160 179a34 34 0 005 54c14 10 33 7 45-5l219-237a33 33 0 000-49zM719 196h131c-24-91-95-160-185-185v131c0 27 25 54 54 54zM129 846l1-747s-7-37 36-33h359v52s-7 76 32 133a191 191 0 00146 84h91v126h66v-191H719a126 126 0 01-127-127V0H155c-51 0-91 40-91 91v767c0 51 40 91 91 91h193v-66H155c0-0-26 4-26-36z + M888 314l-198-198c-8-8-21-8-29 0s-8 21 0 29l163 163-216 0L609 43c0-11-9-20-20-20l-408 0c-11 0-20 9-20 20l0 939c0 11 9 20 20 20l694 0c11 0 20-9 20-20l0-653C894 323 892 318 888 314zM854 961l-653 0L201 63l367 0 0 265c0 11 9 20 20 20l265 0L854 961z 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 @@ -55,11 +62,10 @@ 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 M509 556l93 149h124l-80-79 49-50 165 164-165 163-49-50 79-79h-163l-96-153 41-65zm187-395 165 164-165 163-49-50L726 360H530l-136 224H140v-70h215l136-224h236l-80-79 49-50z - M939 94v710L512 998 85 805V94h-64A21 21 0 010 73v-0C0 61 10 51 21 51h981c12 0 21 10 21 21v0c0 12-10 21-21 21h-64zm-536 588L512 624l109 58c6 3 13 4 20 3a32 32 0 0026-37l-21-122 88-87c5-5 8-11 9-18a32 32 0 00-27-37l-122-18-54-111a32 32 0 00-57 0l-54 111-122 18c-7 1-13 4-18 9a33 33 0 001 46l88 87-21 122c-1 7-0 14 3 20a32 32 0 0043 14z - M236 542a32 32 0 109 63l86-12a180 180 0 0022 78l-71 47a32 32 0 1035 53l75-50a176 176 0 00166 40L326 529zM512 16C238 16 16 238 16 512s222 496 496 496 496-222 496-496S786 16 512 16zm0 896c-221 0-400-179-400-400a398 398 0 0186-247l561 561A398 398 0 01512 912zm314-154L690 622a179 179 0 004-29l85 12a32 32 0 109-63l-94-13v-49l94-13a32 32 0 10-9-63l-87 12a180 180 0 00-20-62l71-47A32 32 0 10708 252l-75 50a181 181 0 00-252 10l-115-115A398 398 0 01512 112c221 0 400 179 400 400a398 398 0 01-86 247z - M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z + M0 0 M1024 1024M51 130H254C264 144 407 384 407 384L357 486 204 231H51C25 231 0 205 0 180 0 155 26 130 51 130ZM1009 511 815 638C800 653 780 653 765 638V536H561L357 893H51C25 893 0 868 0 843 0 817 26 792 51 792H306L509 435H765V332C780 317 800 317 815 332L1009 465C1024 480 1024 501 1009 511Z 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 + M 854 170 c -189 -189 -495 -189 -684 0 s -189 495 0 684 s 495 189 684 0 s 187 -495 0 -684 z M 213 706 c -89 -137 -74 -325 48 -444 c 122 -122 307 -137 444 -48 L 213 706 z m 106 105 l 493 -493 c 89 137 74 325 -48 444 c -120 122 -307 137 -444 48 z + M289 477l147 266C582 460 733 198 1014 20c0 93-15 174 4 247 23 93-23 123-82 180-152 144-287 307-427 463-20 22-35 48-47 67L39 625l250-148z 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 @@ -87,11 +93,14 @@ M908 366h-25V248a18 18 0 00-0-2 20 20 0 00-5-13L681 7 681 7a19 19 0 00-4-3c-0-0-1-1-1-1a29 29 0 00-4-2L671 1a24 24 0 00-5-1H181a40 40 0 00-40 40v326h-25c-32 0-57 26-57 57v298c0 32 26 57 57 57h25v204c0 22 18 40 40 40H843a40 40 0 0040-40v-204h25c32 0 57-26 57-57V424a57 57 0 00-57-57zM181 40h465v205c0 11 9 20 20 20h177v101H181V40zm413 527c0 89-54 143-134 143-81 0-128-61-128-138 0-82 52-143 132-143 84 0 129 63 129 138zm-440 139V433h62v220h108v52h-170zm690 267H181v-193H843l0 193zm18-280a305 305 0 01-91 15c-50 0-86-12-111-37-25-23-39-59-38-99 0-90 66-142 155-142 35 0 62 7 76 13l-13 49c-15-6-33-12-63-12-51 0-90 29-90 88 0 56 35 89 86 89 14 0 25-2 30-4v-57h-42v-48h101v143zM397 570c0 53 25 91 66 91 42 0 65-40 65-92 0-49-23-91-66-91-42 0-66 40-66 93z M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z M824 645V307c0-56-46-102-102-102h-102V102l-154 154 154 154V307h102v338c-46 20-82 67-82 123 0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123zm-51 195c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72zM384 256c0-72-61-133-133-133-72 0-133 61-133 133 0 56 36 102 82 123v266C154 666 118 712 118 768c0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123V379C348 358 384 312 384 256zM323 768c0 41-31 72-72 72-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72zM251 328c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72z + M688 544a48 48 0 110-96 48 48 0 010 96M512 544a48 48 0 110-96 48 48 0 010 96m-176 0a48 48 0 11-0-96A48 48 0 01336 544M805 192H219C186 192 160 219 160 253v471c0 34 26 61 59 61h189c6 0 16 6 18 11l38 72 3 5c11 14 27 23 44 23h0c17 0 33-8 45-23l41-77c2-5 12-11 18-11h190c32 0 59-27 59-61V253C864 219 838 192 805 192 + M544 150H418v-127h127v126zM799 530h-635c-35 0-64-29-64-64v-180c0-35 29-64 64-64h637l119 151-121 157zm-196-210H358v114h247v-114zM416 1021h127v-420H417v420z 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 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 + M133 256A93 93 0 01226 163h499a93 93 0 0193 93v164h76v-164A169 169 0 00725 87H226A169 169 0 0057 256v451a169 169 0 00169 169h214v-76H226A93 93 0 01133 708V256zm458 369a58 58 0 0158-58h180a58 58 0 0158 58v177a58 58 0 01-58 58h-180a58 58 0 01-58-58v-177zm58-134a134 134 0 00-134 134v177a134 134 0 00134 134h180a134 134 0 00134-134v-177a134 134 0 00-134-134h-180zm-262-41H320v76h135a62 62 0 0062-62v-134h-76v67l-150-149-53 54 149 148z M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z M512 0a512 512 0 01512 512 57 57 0 01-114 0 398 398 0 10-398 398 57 57 0 010 114A512 512 0 01512 0zm367 600 121 120a57 57 0 01-80 81l-40-40V967a57 57 0 01-50 57l-7 0a57 57 0 01-57-57v-205l-40 40a57 57 0 01-75 5l-5-5a57 57 0 01-0-80l120-121a80 80 0 01113-0zM512 272a57 57 0 0157 57V499h114a57 57 0 0156 50L740 556a57 57 0 01-57 57H512a57 57 0 01-57-57v-228a57 57 0 0150-57L512 272z M834 0H190C85 0 0 85 0 189v646c0 104 85 189 189 189h645c104 0 189-85 189-189V189C1024 85 939 0 834 0zM658 748c-25 29-62 47-111 54v54h-66v-56c-38-4-72-19-101-44-29-26-43-71-43-135v-28h144v35c0 39 1 63 4 72 3 9 10 14 22 14 10 0 17-3 22-10 5-7 7-16 7-29 0-32-2-55-7-69-5-14-20-29-46-45-44-28-74-48-90-61-16-13-29-31-41-55-12-24-17-50-17-80 0-43 12-77 37-101 24-24 61-40 110-45v-46h66v46c44 6 78 21 100 45 22 24 33 57 33 100 0 6-0 15-1 27H535v-24c0-25-2-42-5-50-3-8-10-12-21-12-9 0-15 3-20 10-4 7-7 17-7 30 0 23 5 38 14 47 9 9 35 27 78 53 37 22 62 39 75 51 13 12 25 28 34 50 9 22 14 48 14 80 0 51-12 92-37 121z @@ -103,7 +112,7 @@ M592 768h-160c-27 0-48-21-48-48V384h-175c-36 0-53-43-28-68L485 11c15-15 40-15 55 0l304 304c25 25 7 68-28 68H640v336c0 27-21 48-48 48zm432-16v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h272v16c0 62 50 112 112 112h160c62 0 112-50 112-112v-16h272c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z M563 555c0 28-23 51-51 51-28 0-51-23-51-51L461 113c0-28 23-51 51-51s51 23 51 51L563 555 563 555zM85 535c0-153 81-287 201-362 24-15 55-8 70 16C371 214 363 245 340 260 248 318 187 419 187 535c0 180 146 325 325 325 180-0 325-146 325-325 0-119-64-223-160-280-24-14-32-46-18-70 14-24 46-32 70-18 125 74 210 211 210 367 0 236-191 427-427 427C276 963 85 772 85 535 M277 85a149 149 0 00-43 292v230a32 32 0 0064 0V555h267A160 160 0 00725 395v-12a149 149 0 10-64-5v17a96 96 0 01-96 96H299V383A149 149 0 00277 85zM228 720a32 32 0 00-37-52 150 150 0 00-53 68 32 32 0 1060 23 85 85 0 0130-39zm136-52a32 32 0 00-37 52 86 86 0 0130 39 32 32 0 1060-23 149 149 0 00-53-68zM204 833a32 32 0 10-55 32 149 149 0 0063 58 32 32 0 0028-57 85 85 0 01-36-33zm202 32a32 32 0 00-55-32 85 85 0 01-36 33 32 32 0 0028 57 149 149 0 0063-58z - m224 154a166 166 0 00-166 166v192a166 166 0 00166 166h64v-76h-64a90 90 0 01-90-90v-192a90 90 0 0190-90h320a90 90 0 0190 90v192a90 90 0 01-90 90h-128v77h128a166 166 0 00166-167v-192a166 166 0 00-166-166h-320zm166 390a90 90 0 0190-90h128v-76h-128a166 166 0 00-166 166v192a166 166 0 00166 166h320a166 166 0 00166-166v-192a166 166 0 00-166-166h-64v77h64a90 90 0 0190 90v192a90 90 0 01-90 90h-320a90 90 0 01-90-90v-192z + M912 382l-32-94-184 75L708 160H607l12 202-184-75-32 94 193 53-133 150 78 61 116-168L773 646l78-61-133-150zM256 769m-71 0a71 71 0 10141 0 71 71 0 10-141 0Z M512 128M706 302a289 289 0 00-173 44 27 27 0 1029 46 234 234 0 01125-36c23 0 45 3 66 9 93 28 161 114 161 215C914 704 813 805 687 805H337C211 805 110 704 110 580c0-96 61-178 147-210C282 263 379 183 495 183a245 245 0 01210 119z M364 512h67v108h108v67h-108v108h-67v-108h-108v-67h108v-108zm298-64A107 107 0 01768 555C768 614 720 660 660 660h-108v-54h-108v-108h-94v108h-94c4-21 22-47 44-51l-1-12a75 75 0 0171-75a128 128 0 01239-7a106 106 0 0153-14z M115 386l19 33c17 29 44 50 76 60l116 33c34 10 58 41 58 77v80c0 22 12 42 32 52s32 30 32 52v78c0 31 30 54 60 45 32-9 57-35 65-68l6-22c8-34 30-63 61-80l16-9c30-17 48-49 48-83v-17c0-25-10-50-28-68l-8-8c-18-18-42-28-68-28H514c-22 0-44-6-64-17l-69-39c-9-5-15-13-18-22-6-19 2-40 20-49l12-6c13-7 29-8 43-3l46 15c16 5 34-1 44-15 9-14 8-33-2-46l-27-33c-20-24-20-59 1-83l31-37c18-21 20-50 7-73l-5-8c-7-0-14-1-21-1-186 0-343 122-396 290zM928 512c0-74-19-143-53-203L824 330c-31 13-48 48-37 80l34 101c7 21 24 37 45 42l58 15c2-18 4-36 4-55zM0 512a512 512 0 111024 0 512 512 0 11-1024 0z @@ -116,17 +125,16 @@ 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 + M622 718v137q0 9-7 16t-16 7H462q-9 0-16-7t-7-16v-137q0-9 7-16t16-7h137q9 0 16 7t7 16zm181-343q0 31-9 58t-20 44-31 34-33 25-35 20q-23 13-39 37t-16 38q0 10-7 19t-16 9H459q-9 0-15-11T439 626v-26q0-47 37-89T558 449q34-15 48-32t14-43q0-24-27-42T532 313q-37 0-62 17-20 14-61 66-7 9-18 9-7 0-14-5L283 329q-7-6-9-14t3-16q91-152 265-152 46 0 92 18t83 47 61 73 23 91z 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 M363 491h64v107h107v64h-107v107h-64v-107h-107v-64h107v-107zm149-235 256 128-256 128-64-32v-11H427l-171-85 256-128zm256 384-256 128-64-32v-53h64l0 0 0-0h43v-21l128-64 85 43zm0-128-213 107v-43h-107v-53l64 32 171-85 85 43zm-512 0 85-43v85l-85-43z M447 561a26 26 0 0126 26v171H421v-171a26 26 0 0126-26zm-98 65a26 26 0 0126 26v104H323v-104a26 26 0 0126-26zm0 0M561 268a32 32 0 0132 30v457h-65V299a32 32 0 0132-32zm0 0M675 384a26 26 0 0126 26v348H649v-350a26 26 0 0126-24zm0 0M801 223v579H223V223h579M805 171H219A49 49 0 00171 219v585A49 49 0 00219 853h585A49 49 0 00853 805V219A49 49 0 00805 171z - M576 160H448c-18 0-32-14-32-32s14-32 32-32h128c18 0 32 14 32 32s-14 32-32 32zm243 186 36-36c13-13 13-33 0-45s-33-13-45 0l-33 33C708 233 614 192 512 192c-212 0-384 172-384 384s172 384 384 384 384-172 384-384c0-86-29-166-77-230zM544 894V864c0-18-14-32-32-32s-32 14-32 32v30C329 879 209 759 194 608H224c18 0 32-14 32-32s-14-32-32-32h-30C209 393 329 273 480 258V288c0 18 14 32 32 32s32-14 32-32v-30C695 273 815 393 830 544H800c-18 0-32 14-32 32s14 32 32 32h30C815 759 695 879 544 894zm108-471-160 128c-14 11-16 31-5 45 6 8 16 12 25 12 7 0 14-2 20-7l160-128c14-11 16-31 5-45-11-14-31-16-45-5z 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 - M512 0C230 0 0 230 0 512c0 145 60 282 166 375L90 1024H512c282 0 512-230 512-512S794 0 512 0z + M0 512M1024 512M512 0M512 1024M213 290V213H43V811h171v-77H128V290zM811 290V213h171V811h-171v-77h85V290zM751 393H245v85h506v-85zM614 546H245v85h369v-85z 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 diff --git a/src/Resources/Images/ShellIcons/cmux.png b/src/Resources/Images/ShellIcons/cmux.png new file mode 100644 index 000000000..b847bbb70 Binary files /dev/null and b/src/Resources/Images/ShellIcons/cmux.png differ diff --git a/src/Resources/Images/ShellIcons/mux0.png b/src/Resources/Images/ShellIcons/mux0.png new file mode 100644 index 000000000..e16b37400 Binary files /dev/null and b/src/Resources/Images/ShellIcons/mux0.png differ diff --git a/src/Resources/Images/ShellIcons/otty.png b/src/Resources/Images/ShellIcons/otty.png new file mode 100644 index 000000000..e2ce5897f Binary files /dev/null and b/src/Resources/Images/ShellIcons/otty.png differ diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 530dcd505..57a344c63 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -5,83 +5,84 @@ Info Über SourceGit - Hinweise zur Veröffentlichung - Open Source & freier Git GUI Client + Veröffentlichungsdatum: {0} + Versionshinweise + Open Source & freier Git GUI-Client Zu ignorierende Datei(en) hinzufügen Muster: Speichern in Datei: Worktree hinzufügen Ordner: - Pfad für diesen Worktree. Relativer Pfad wird unterstützt. - Branch Name: - Optional. Standard ist der Zielordnername. + Pfad für diesen Worktree. Relative Pfade werden unterstützt. + Branch-Name: + Optional. Standard ist der Name des Zielordners. Branch verfolgen: Remote-Branch verfolgen Was auschecken: Neuen Branch erstellen Existierender Branch - OpenAI Assistent - Neu generieren - Verwende OpenAI, um Commit-Nachrichten zu generieren - Als Commit-Nachricht verwenden + AI-Assistent + Modell + NEU GENERIEREN + Verwende AI, um Commit-Nachrichten zu generieren SourceGit minimieren Alles anzeigen - Patch - Patch-Datei: Wähle die anzuwendende .patch-Datei - Ignoriere Leerzeichenänderungen + Ignoriere Leerzeichen-Änderungen Patch anwenden Leerzeichen: Stash anwenden Nach dem Anwenden löschen Änderungen des Index wiederherstellen Stash: - Archivieren... + Archivieren… Speichere Archiv in: - Wähle Archivpfad aus + Wähle einen Archivpfad aus Revision: Archiv erstellen - SourceGit Askpass + SourceGit-Askpass Passphrase eingeben: ALS UNVERÄNDERT ANGENOMMENE DATEIEN KEINE ALS UNVERÄNDERT ANGENOMMENEN DATEIEN - Bild laden... + Bild laden… Aktualisieren BINÄRE DATEI WIRD NICHT UNTERSTÜTZT!!! Bisect Abbrechen Schlecht - Bisecting. Ist der aktuelle HEAD gut oder schlecht? Gut Überspringen - Bisecting. Aktuellen Commit als gut oder schlecht markieren und einen anderen auschecken. + Bisecting. Aktuellen Commit als gut oder schlecht markieren und einen anderen auschecken. + Bisecting. Ist der aktuelle HEAD gut oder schlecht? Blame Blame auf vorheriger Revision + Leerzeichen-Änderungen ignorieren BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! - Auschecken von ${0}$... - Mit ${0}$ vergleichen - Mit Worktree vergleichen + Auschecken von ${0}$… + Ausgewählte 2 Branches vergleichen + Vergleichen mit… + Vergleichen mit HEAD Branch-Namen kopieren - PR erstellen... - PR für Upstream ${0}$ erstellen... + 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... + Lösche ${0}$… + Lösche ausgewählte {0} Branches + Beschreibung für ${0}$ bearbeiten… Fast-Forward zu ${0}$ - Fetche ${0}$ in ${1}$ hinein... - Git Flow - Abschließen ${0}$ - Merge ${0}$ in ${1}$ hinein... + Fetche ${0}$ in ${1}$ hinein… + Git Flow – Abschließen ${0}$ + Interaktives Rebase von ${0}$ auf ${1}$ + Merge ${0}$ in ${1}$ hinein… Merge ausgewählte {0} Branches in aktuellen hinein Pull ${0}$ - Pull ${0}$ in ${1}$ hinein... + Pull ${0}$ in ${1}$ hinein… Push ${0}$ - Rebase ${0}$ auf ${1}$... - Benenne ${0}$ um... - Setze ${0}$ zurück auf ${1}$... + Rebase ${0}$ auf ${1}$… + Benenne ${0}$ um… + Setze ${0}$ zurück auf ${1}$… Zu ${0}$ wechseln (Worktree) - Setze verfolgten Branch... - Branch Vergleich + Setze verfolgten Branch… {0} Commit(s) voraus {0} Commit(s) voraus, {1} Commit(s) zurück {0} Commit(s) zurück @@ -95,89 +96,81 @@ Auf Vorgänger-Revision zurücksetzen Auf diese Revision zurücksetzen Generiere Commit-Nachricht - ANZEIGE MODUS ÄNDERN + Merge (integriert) + Merge (extern) + Datei(en) auf ${0}$ zurücksetzen + ANZEIGEMODUS ÄNDERN Zeige als Datei- und Ordnerliste Zeige als Pfadliste - Zeige als Dateisystembaum + Zeige als Dateisystem-Baumstruktur Ändere URL des Submoduls - Submodule: + Submodul: URL: Branch auschecken - Commit auschecken - Commit: - Warnung: Beim Auschecken eines Commits wird dein HEAD losgelöst (detached) sein! Lokale Änderungen: - Verwerfen - Stashen & wieder anwenden - Alle Submodule updaten Branch: - Dein aktueller HEAD enthält Commit(s) ohne Verbindung zu einem Branch/Tag. Möchtest du trotzdem fortfahren? - Auschecken & Fast-Forward - Fast-Forward zu: + Dein aktueller HEAD enthält Commit(s) ohne Verbindung zu einem Branch / Tag. Möchtest du trotzdem fortfahren? + Die folgenden Submodule müssen aktualisiert werden:{0}Möchtest du sie aktualisieren? + Auschecken & Fast Forward + Fast Forward zu: Cherry Pick Quelle an Commit-Nachricht anhängen Commit(s): Alle Änderungen committen 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. + Normalerweise ist es nicht möglich, einen Merge zu cherry-picken, da unklar ist, welche Seite des Merges als Hauptlinie anzusehen 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? - Remote Repository klonen - Extra Parameter: - Zusätzliche Argumente für das Klonen des Repositories. Optional. + Du versuchst, alle Stashes zu löschen. Möchtest du wirklich fortfahren? + Remote-Repository klonen + Extra-Parameter: + Zusätzliche Argumente für das Klonen des Repositorys. Optional. Lokaler Name: Repository-Name. Optional. - Übergeordneter Ordner: + Übergeordnetes Verzeichnis: Submodule initialisieren und aktualisieren - Repository URL: + Repository-URL: SCHLIESSEN Editor Commit auschecken Diesen Commit cherry-picken - Mehrere cherry-picken - Mit HEAD vergleichen - Mit Worktree vergleichen - Author + Mehrere cherry-picken… + Vergleiche mit HEAD + Vergleiche mit Worktree + Autor Commit-Nachricht Committer SHA Betreff Benutzerdefinierte Aktion - Commit entfernen Interaktives Rebase - Entfernen... - Bearbeiten... - Fixup in den Vorgänger... + Entfernen… + Bearbeiten… + Fixup in den Vorgänger… Interaktives Rebase von ${0}$ auf ${1}$ - Umformulieren... - Squash in den Vorgänger... - Merge in ${0}$ hinein - Merge ... + Umformulieren… + Squash in den Vorgänger… + Merge… Push ${0}$ zu ${1}$ Rebase ${0}$ auf ${1}$ Setze ${0}$ auf ${1}$ zurück Commit rückgängig machen - Umformulieren - Als Patch speichern... - Squash in den Vorgänger - Fixup in den Vorgänger + Als Patch speichern… ÄNDERUNGEN geänderte Datei(en) - Änderungen durchsuchen... + Änderungen durchsuchen… DATEIEN - LFS DATEI - Dateien durchsuchen... - Submodule + LFS-DATEI + Dateien durchsuchen… + Submodul INFORMATION AUTOR - NACHFOLGER 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. + E-Mail-Adresse kopieren + Namen kopieren + Namen & E-Mail-Adresse kopieren + Zeigt nur die ersten 100 Änderungen. Alle Änderungen siehe Registerkarte ‚ÄNDERUNGEN‘. Schlüssel: COMMIT-NACHRICHT VORGÄNGER @@ -186,32 +179,32 @@ Unterzeichner: Im Browser öffnen Commit-Nachricht eingeben. Leerzeile zum Trennen von Betreff und Beschreibung verwenden! - Betreff - Repository Einstellungen - COMMIT TEMPLATE + BETREFF + Vergleichen + Repository-Einstellungen + COMMIT-VORLAGE 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: + +${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 + Vorlageninhalt: + Vorlagenname: BENUTZERDEFINIERTE AKTION Argumente: 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 - +${REPO} Repository-Pfad +${REMOTE} Selektierter Remote oder selektierter Branch-Remote +${BRANCH} Selektierter Branch, ohne ${REMOTE}-Teil für Remote-Branches +${BRANCH_FRIENDLY_NAME} Benutzerfreundlicher Name des selektierten Branches, enthält ${REMOTE}-Teil für Remote-Branches +${SHA} Hash des selektierten Commits +${TAG} Ausgewählter Tag +${FILE} Ausgewählte Datei, relativ zum Stammverzeichnis des Repositorys +$1, $2, … Werte der Eingabe-Steuerelemente Ausführbare Datei: Eingabe-Steuerelemente: Bearbeiten @@ -224,79 +217,78 @@ Repository Tag Auf Beenden der Aktion warten - Email Adresse - Email Adresse + E-Mail-Adresse + E-Mail-Adresse GIT + Vor dem Auto-Aktualisieren von Submodulen fragen Remotes automatisch fetchen Minute(n) - Typen für konventionellen Commit - Standard Remote - Bevorzugter Merge Modus + Konventionelle Commit-Typen + 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 - Beispiel für Gitlab Issue Regel hinzufügen - Beispiel für Gitlab Merge Request Regel hinzufügen + 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 + Beispiel für Gitlab-Issue-Regel hinzufügen + Beispiel für Gitlab-Merge-Request-Regel hinzufügen Beispiel für Jira-Regel hinzufügen Neue Regel - Ticketnummer Regex-Ausdruck: + Regulärer Ausdruck für Ticket-ID: Name: - Regel per Datei .issuetracker teilen + Diese Regel per .issuetracker-Datei teilen Ergebnis-URL: - Verwende bitte $1, $2 um auf Regex-Gruppenwerte zuzugreifen. + 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 Services verfügbar sind wird ein Kontextmenü zur Auswahl angezeigt. - HTTP Proxy - HTTP Proxy für dieses Repository + Bevorzugter Dienst: + Der ausgewählte ‚Bevorzugte Dienst‘ wird nur in diesem Repository gesetzt und verwendet. Wenn keiner gesetzt ist und mehrere Dienste verfügbar sind, wird ein Kontextmenü zur Auswahl angezeigt. + HTTP-Proxy + HTTP-Proxy für dieses Repository Benutzername Benutzername für dieses Repository Bearbeite Steuerelemente für benutzerdefinierte Aktionen Wert bei Markierung: - Wenn markiert, wird dieser Wert in Kommandozeilen-Argumenten benutzt + Falls ausgewählt, wird dieser Wert in Kommandozeilen-Argumenten benutzt Beschreibung: Standardwert: Auf Ordner einschränken Bezeichnung: Einträge: - Nutze '|', um Einträge zu trennen + 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 + Arbeitsumgebungen Farbe Name - Zuletzt geöffnete Tabs beim Starten wiederherstellen + Offene Registerkarten beim Programmstart wiederherstellen WEITER Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty)? ALLES STAGEN & COMMITTEN Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty) oder alle Änderungen stagen und dann committen? Neustart erforderlich - Du musst dieses App neustarten, um Änderungen zu übernehmen. + Du musst dieses Programm neustarten, um die Änderungen zu übernehmen. Konventionelle Commit-Hilfe Breaking Change: Geschlossenes Ticket: Änderungen im Detail: - Gültigkeitsbereich: + Geltungsbereich: Kurzbeschreibung: Typ der Änderung: Kopieren Gesamten Text kopieren Ganzen Pfad kopieren Pfad kopieren - Branch erstellen... + Branch erstellen… Basierend auf: Erstellten Branch auschecken Lokale Änderungen: - Verwerfen - Stashen & wieder anwenden Neuer Branch-Name: - Branch-Name + Einen Branch-Namen eingeben Lokalen Branch erstellen Überschreibe existierenden Branch - Tag erstellen... + Tag erstellen… Neuer Tag auf: Mit GPG signieren Anmerkung: @@ -304,81 +296,79 @@ Tag-Name: Empfohlenes Format: v1.0.0-alpha Nach Erstellung auf alle Remotes pushen - Erstelle neuen Tag + Neuen Tag anlegen Art: Mit Anmerkung Ohne Anmerkung - Halte Strg gedrückt, um direkt auszuführen + Halte ‚Strg‘ gedrückt, um direkt auszuführen Ausschneiden - De-initialisiere Submodul - Erzwinge De-Initialisierung, selbst wenn es lokale Änderungen enthält. + Verwerfen + Nichts tun + Stashen & wieder anwenden + Deinitialisiere Submodul + Erzwinge Deinitialisierung, selbst wenn lokale Änderungen enthalten sind. Submodul: Branch löschen Branch: - Du löscht gerade einen Remote-Branch!!! - Auch Remote-Branch ${0}$ löschen + Du bist dabei, einen Remote-Branch zu löschen!!! Mehrere Branches löschen - Du versuchst mehrere Branches auf einmal zu löschen. Kontrolliere noch einmal vor dem Fortfahren! + Du versuchst, mehrere Branches auf einmal zu löschen. Kontrolliere dies sorgfältig, bevor du fortfährst! 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! + Von Remotes löschen + Du versuchst, mehrere Tags auf einmal zu löschen. Kontrolliere dies sorgfältig, bevor du fortfährst! Remote löschen Remote: Pfad: Ziel: Alle Nachfolger werden aus der Liste entfernt. Dadurch wird es nur aus der Liste entfernt, nicht von der Festplatte! - Bestätige löschen von Gruppe - Bestätige löschen von Repository + Bestätige Löschen von Gruppe + Bestätige Löschen von Repository Lösche Submodul - Submodul Pfad: + Submodul-Pfad: Tag löschen Tag: - Von Remote Repositories löschen + Von Remote-Repositorys löschen BINÄRER VERGLEICH - Dateimodus geändert - Erste Differenz - Ignoriere Leerzeichenänderungen - Überblenden - Differenz - Nebeneinander - Wischen - Letzte Differenz - LFS OBJEKT ÄNDERUNG + Erster Unterschied + Ignoriere Leerzeichen-Änderungen + ÜBEREINANDER + DIFFERENZ + NEBENEINANDER + WISCHEN + Letzter Unterschied + LFS-OBJEKT-ÄNDERUNG NEU - Nächste Änderung - KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN + Nächster Unterschied + KEINE ÄNDERUNG ODER NUR ZEILENENDE-ÄNDERUNGEN ALT - Vorherige Änderung + Vorheriger Unterschied Als Patch speichern Zeige versteckte Symbole Nebeneinander SUBMODUL GELÖSCHT NEU - Seiten wechseln - Syntax Hervorhebung + Seiten tauschen + Syntax-Hervorhebung Zeilenumbruch - Öffne in Merge Tool + Öffne in Merge-Tool Alle Zeilen anzeigen Weniger Zeilen anzeigen Mehr Zeilen anzeigen - WÄHLE EINE DATEI AUS UM ÄNDERUNGEN ANZUZEIGEN + WÄHLE EINE DATEI AUS, UM ÄNDERUNGEN ANZUZEIGEN Verzeichnisverlauf Hat lokale Änderungen Abweichungen vom Upstream - Schon aktuell + Bereits aktuell Änderungen verwerfen Alle Änderungen in der Arbeitskopie. Änderungen: Ignorierte Dateien einbeziehen Nicht-verfolgte Dateien einbeziehen - Insgesamt {0} Änderungen werden verworfen + {0} Änderungen werden verworfen Du kannst das nicht rückgängig machen!!! - Commit entfernen - Commit: - Neuer HEAD: - Beschreibung eines Branches bearbeiten + Beschreibung des Branches bearbeiten Ziel-Branch: Lesezeichen: Neuer Name: @@ -389,120 +379,117 @@ Dieses Repository Fetch Alle Remotes fetchen - Aktiviere '--force' Option + Überschreiben lokaler Refs erzwingen Ohne Tags fetchen Remote: Remote-Änderungen fetchen Als unverändert betrachten Benutzerdefinierte Aktion - Verwerfen... - Verwerfe {0} Dateien... + Verwerfen… + Verwerfe {0} Dateien… Löse mit ${0}$ - Als Patch speichern... + Als Patch speichern… Stagen - {0} Dateien stagen - Stash... - {0} Dateien stashen... - Unstage + Stage {0} Dateien + Stashen… + Stashe {0} Dateien… + Unstagen {0} Dateien unstagen - "Meine" verwenden (checkout --ours) - "Ihre" verwenden (checkout --theirs) - Datei Historie - ÄNDERUNGEN + Meine verwenden (checkout --ours) + Deren verwenden (checkout --theirs) + Dateiverlauf + ÄNDERUNG INHALT - Git-Flow + Git Flow Development-Branch: Feature: - Feature-Prefix: - FLOW - Feature abschließen - FLOW - Hotfix abschließen - FLOW - Release abschließen + Feature-Präfix: + FLOW – Feature fertigstellen + FLOW – Hotfix fertigstellen + FLOW – Release fertigstellen Ziel: - Push zu Remote(s) nach Abschluss Squash beim Merge Hotfix: - Hotfix-Prefix: - Git-Flow initialisieren + Hotfix-Präfix: + Git Flow initialisieren Branch behalten Production-Branch: Release: - Release-Prefix: - Feature starten... - FLOW - Feature starten - Hotfix starten... - FLOW - Hotfix starten - Name eingeben - Release starten... - FLOW - Release starten - Versions-Tag-Prefix: + Release-Präfix: + Feature beginnen… + FLOW – Feature beginnen + Hotfix beginnen… + FLOW – Hotfix beginnen + Namen eingeben + Release beginnen… + FLOW – Release beginnen + Versions-Tag-Präfix: Git LFS - Verfolgungsmuster hinzufügen... + Verfolgungsmuster hinzufügen… Muster ist ein Dateiname Eigenes Muster: Verfolgungsmuster zu Git LFS hinzufügen Fetch - Führt `git lfs fetch` aus um Git LFS Objekte herunterzuladen. Das aktualisiert nicht die Arbeitskopie. - LFS Objekte fetchen - Installiere Git LFS Hooks + Führt `git lfs fetch` aus, um Git-LFS-Objekte herunterzuladen. Dies aktualisiert nicht die Arbeitskopie. + LFS-Objekte fetchen + Installiere Git-LFS-Hooks Sperren anzeigen Keine gesperrten Dateien Sperre Zeige nur meine Sperren - LFS Sperren + LFS-Sperren Entsperren - Alle meine Dateien entsperren - Sollen alle meine Dateien entsperrt werden? + Alle meine Sperrungen aufheben + Sollen alle deine Dateien entsperrt werden? Erzwinge entsperren Prune - Führt `git lfs prune` aus um alte LFS Dateien vom lokalen Speicher zu löschen + Führt `git lfs prune` aus, um alte LFS-Dateien aus dem lokalen Speicher zu löschen Pull - Führt `git lfs pull` aus um alle Git LFS-Dateien für aktuellen Ref & Checkout herunterzuladen - LFS Objekte pullen + 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 - LFS Objekte pushen + Pushe große Dateien in der Warteschlange zum Git-LFS-Endpunkt + LFS-Objekte pushen Remote: - Verfolge alle '{0}' Dateien + Verfolge alle ‚{0}‘ Dateien Verfolge alle *{0} Dateien - Verlauf + VERLAUF AUTOR - AUTOR ZEITPUNKT + AUTOR-ZEITPUNKT + COMMIT-ZEITPUNKT VERLAUF & COMMIT-NACHRICHT SHA - COMMIT ZEITPUNKT {0} COMMITS AUSGEWÄHLT - Halte 'Strg' oder 'Umschalt', um mehrere Commits auszuwählen. + SPALTEN ANZEIGEN + Halte ‚Strg‘ oder ‚Umschalt‘, um mehrere Commits auszuwählen. Halte ⌘ oder ⇧, um mehrere Commits auszuwählen TIPPS: - Tastaturkürzel Referenz + Tastaturkürzel-Referenz GLOBAL Klone neues Repository - Aktuellen Tab schließen - Zum nächsten Tab wechseln - Zum vorherigen Tab wechseln - Neuen Tab erstellen + Aktuelle Registerkarte schließen + Zur nächsten Registerkarte wechseln + Zur vorherigen Registerkarte wechseln + Neue Registerkarte erstellen Einstellungen öffnen - Aktiven Tab wechseln + Auswahlmenü für Arbeitsumgebungen anzeigen + Aktive Registerkarte wechseln + Herein- / herauszoomen REPOSITORY Gestagte Änderungen committen Gestagte Änderungen committen und pushen Alle Änderungen stagen und committen Fetch, wird direkt ausgeführt - Dashboard Modus (Standard) + Dashboard-Modus (Standard) + Springe zum Vorgänger des ausgewählten Commits Befehlspalette öffnen Commit-Suchmodus Pull, wird direkt ausgeführt Push, wird direkt ausgeführt Erzwinge Neuladen des Repositorys - Wechsle zu 'Änderungen' - Wechsle zu 'Verlauf' - Wechsle zu 'Stashes' - TEXTEDITOR - Suchpanel schließen - Suche nächste Übereinstimmung - Suche vorherige Übereinstimmung - Öffne mit externem Diff/Merge Tool - Öffne Suchpanel + Wechsle zu ‚Änderungen‘ + Wechsle zu ‚Verlauf‘ + Wechsle zu ‚Stashes‘ Verwerfen Stagen Unstagen @@ -520,20 +507,36 @@ Lokale Änderungen stashen & wieder anwenden Auf: Drag & Drop zum Anordnen der Commits - Ziel Branch: + Ziel-Branch: Link kopieren - In Browser öffnen + Im Browser öffnen Befehle FEHLER INFO Repositorys öffnen - Tabs - Arbeitsplätze + Registerkarten + Arbeitsumgebungen Branch mergen Merge-Nachricht anpassen Ziel-Branch: - Merge Option: + Merge-Option: Quelle: + Zuerst meine, dann deren + Zuerst deren, dann meine + BEIDE VERWENDEN + Alle Konflikte gelöst + {0} Konflikt(e) verbleibend + MEINE + Nächster Konflikt + Vorheriger Konflikt + ERGEBNIS + SPEICHERN & STAGEN + DEREN + Merge-Konflikt – {0} + Ungespeicherte Änderungen verwerfen? + MEINE VERWENDEN + DEREN VERWENDEN + RÜCKGÄNGIG MACHEN Merge (mehrere) Alle Änderungen committen Strategie: @@ -541,24 +544,26 @@ Submodul verschieben Verschieben zu: Submodul: - Verschiebe Repository Knoten + Verschiebe Repository-Knoten Wähle Vorgänger-Knoten für: Name: - Git wurde NICHT konfiguriert. Gehe bitte zuerst in die [Einstellungen] und konfiguriere Git. + NEIN + Git wurde NICHT konfiguriert. Gehe bitte zunächst in die [Einstellungen] und konfiguriere es. Öffnen Standard-Texteditor (System) - App-Daten Ordner öffnen + Programmdaten-Ordner öffnen Datei öffnen - Öffne in Merge Tool + Öffnen in externem Merge-Tool Optional. - Neue Seite erstellen - Lesezeichen - Tab schließen - Andere Tabs schließen - Rechte Tabs schließen + Neue Registerkarte erstellen + Registerkarte schließen + Andere Registerkarten schließen + Registerkarten zur Rechten schließen Kopiere Repository-Pfad + Bearbeiten + In Arbeitsumgebung verschieben Aktualisieren - Repositories + Repositorys Einfügen Vor {0} Tagen Vor 1 Stunde @@ -571,81 +576,78 @@ Vor {0} Jahren Gestern Einstellungen - OPEN AI - System-Prompt für Diff Analyse - API Schlüssel - System-Prompt für Erstellung von Commit-Nachricht - Modell + AI + API-Schlüssel Name - Der eingegebene Wert ist der Name der Environment-Variable, von der der Schlüssel gelesen wird + Der eingegebene Wert ist der Name der Umgebungsvariable, aus der der API-Schlüssel gelesen wird Server - Streaming aktivieren DARSTELLUNG Standardschriftart - Editor Tab Breite + Editor-Tab-Breite Schriftgröße Standard Texteditor - Monospace-Schriftart + Festbreiten-Schriftart Design Design-Anpassungen - Scrollbars automatisch ausblenden - Fixe Tab-Breite in Titelleiste - Verwende nativen Fensterrahmen - DIFF/MERGE TOOL + Bildlaufleisten automatisch ausblenden + Fixe Tab-Breite in Titelleiste verwenden + Nativen Fensterrahmen verwenden + DIFF- / MERGE-TOOL + Argumente für Diff + Verfügbare Variablen: $LOCAL, $REMOTE + Argumente für Merge + Verfügbare Variablen: $BASE, $LOCAL, $REMOTE, $MERGED Installationspfad - Pfad zum Diff/Merge Tool + Pfad zum Diff- / Merge-Tool Tool ALLGEMEIN - Beim Starten nach Updates suchen + Beim Programmstart nach Aktualisierungen 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 + Standardmäßig ‚ÄNDERUNGEN‘-Ansicht anzeigen + Standardmäßig Registerkarte ‚ÄNDERUNGEN‘ in Commit-Details anzeigen + Zeige Tags im Commit-Verlauf Längenvorgabe für Commit-Nachrichten Standard-Avatar im GitHub-Stil generieren GIT Aktiviere Auto-CRLF Standard Klon-Ordner - Benutzer Email - Globale Git Benutzer Email + Benutzer-E-Mail + Globale Git Benutzer-E-Mail Aktiviere --prune beim Fetchen - Aktiviere --ignore-cr-at-eol beim Diff - Diese App setzt Git (>= 2.25.1) voraus + Aktiviere --ignore-cr-at-eol für Text-Diff + Dieses Programm setzt Git (>= 2.25.1) voraus Installationspfad - Aktiviere HTTP SSL Verifizierung + Aktiviere HTTP-SSL-Verifizierung git-credential-libsecret statt git-credential-manager verwenden Benutzername - Globaler Git Benutzername - Git Version - GPG SIGNIERUNG + Globaler Git-Benutzername + Git-Version + GPG-SIGNIERUNG Commit-Signierung - GPG Format - GPG Installationspfad - Pfad zum GPG Programm + GPG-Format + GPG-Installationspfad + Pfad zum GPG-Programm Tag-Signierung - Benutzer Signierungsschlüssel - GPG Benutzer Signierungsschlüssel + Benutzer-Signierungsschlüssel + GPG-Signierungsschlüssel des Benutzers INTEGRATIONEN - SHELL/TERMINAL + SHELL / TERMINAL + Argumente + Bitte verwende ‚.‘ für das aktuelle Verzeichnis Pfad - Shell/Terminal - Remote löschen + Shell / Terminal + Remote bereinigen Ziel: - Worktrees löschen - Worktree Informationen in `$GIT_COMMON_DIR/worktrees` löschen + Worktrees bereinigen + Worktree-Informationen in `$GIT_COMMON_DIR/worktrees` bereinigen Pull Remote-Branch: Lokaler Branch: Lokale Änderungen: - Verwerfen - Stashen & wieder anwenden - Alle Submodule aktualisieren Remote: Pull (Fetch & Merge) Rebase anstatt Merge verwenden @@ -665,8 +667,8 @@ Zu allen Remotes pushen Remote: Tag: - Push zu NEUEM Branch - Eingabe des Namens des neuen Remote-Branch: + Push zu einem NEUEN Branch + Name des neuen Remote-Branches: Schließen Aktuellen Branch rebasen Lokale Änderungen stashen & wieder anwenden @@ -674,13 +676,13 @@ Remote hinzufügen Remote bearbeiten Name: - Remote Name - Repository URL: - Remote Git Repository URL + Remote-Name + Repository-URL: + Remote Git-Repository-URL URL kopieren Benutzerdefinierte Aktion - Löschen... - Bearbeiten... + Löschen… + Bearbeiten… Fetch Im Browser öffnen Prune @@ -692,22 +694,22 @@ Eindeutiger Name für diesen Branch Branch: ABBRECHEN - Änderungen automatisch von Remote fetchen... + Änderungen automatisch von Remote fetchen… Sortieren Nach Commit-Datum Nach Name Aufräumen (GC & Prune) - Führt `git gc` auf diesem Repository aus. + Führt `git gc` in diesem Repository aus. Filter aufheben Löschen - Repository Einstellungen + Repository-Einstellungen WEITER Benutzerdefinierte Aktionen Keine benutzerdefinierten Aktionen Dashboard Alle Änderungen verwerfen Öffne im Datei-Browser - Suche Branches/Tags/Submodule + Suche Branches / Tags / Submodule Sichtbarkeit im Verlauf Aufheben Im Verlauf ausblenden @@ -715,22 +717,22 @@ LAYOUT Horizontal Vertikal - COMMIT SORTIERUNG - Commit Zeitpunkt - Topologie + COMMIT-SORTIERUNG + Commit-Zeitpunkt + Topologisch LOKALE BRANCHES - Mehr Optionen... + Mehr Optionen… Zum HEAD wechseln Erstelle Branch BENACHRICHTIGUNGEN LÖSCHEN - Nur aktuellen Branch hervorheben + Als Ordner öffnen Öffne in {0} Öffne in externen Tools REMOTES - REMOTE HINZUFÜGEN + Remote hinzufügen + AUFLÖSEN Commit suchen Autor - Committer Inhalt Commit-Nachricht Pfad @@ -740,70 +742,70 @@ Nur ersten Vorgänger anzeigen ANZEIGE-FLAGS Verlorene Commits anzeigen - Submodule als Baum anzeigen - Zeige Tags als Baum + Zeige Submodule als Baumstruktur + Zeige Tags als Baumstruktur ÜBERSPRINGEN Statistiken SUBMODULE - SUBMODUL HINZUFÜGEN - SUBMODUL AKTUALISIEREN + Submodul hinzufügen + Submodul aktualisieren TAGS - NEUER TAG + Neuer Tag Nach Erstelldatum - Nach Namen - Sortiere + Nach Name + Sortieren Öffne im Terminal - Verwende relative Zeitangeben Logs ansehen - Öffne '{0}' im Browser + Öffne ‚{0}‘ im Browser WORKTREES - WORKTREE HINZUFÜGEN - PRUNE - Git Repository URL + Worktree hinzufügen + Prune + Git-Repository-URL Aktuellen Branch auf Revision zurücksetzen Rücksetzmodus: Verschiebe zu: Aktueller Branch: - Reset Branch (ohne Checkout) + Branch zurücksetzen (ohne Checkout) Auf: Branch: - Zeige im Datei-Explorer + Zeigen im Datei-Explorer Commit rückgängig machen Commit: - Commit Änderungen rückgängig machen - Commit-Nachricht umformulieren - Bitte warten... + Commit-Änderungen rückgängig machen + In Bearbeitung. Bitte warten… SPEICHERN - Speichern als... + Speichern als… Patch wurde erfolgreich gespeichert! - Durchsuche Repositories - Hauptverzeichnis: + Durchsuche Repositorys + Stammverzeichnis: Anderes benutzerdefiniertes Verzeichnis durchsuchen - Suche nach Updates... - Neue Version ist verfügbar: - Suche nach Updates fehlgeschlagen! - Download + Suche nach Aktualisierungen… + Eine neue Version dieses Programms ist verfügbar: + Derzeitige Version: + Suche nach Aktualisierungen fehlgeschlagen! + Herunterladen Diese Version überspringen - Software Update - Es sind momentan keine Updates verfügbar. + Veröffentlichungsdatum neue Version: + Software-Update + Momentan sind keine Aktualisierungen verfügbar. Branch des Submoduls setzen Submodul: Aktueller: Ändern zu: - Optional. Leerlassen für Standard + Optional. Leer lassen für Standardwert. Setze verfolgten Branch Branch: - Upstream Verfolgung aufheben + Upstream-Verfolgung aufheben Upstream: SHA kopieren Zum Commit wechseln - SSH privater Schlüssel: - Pfad zum privaten SSH Schlüssel + Privater SSH-Schlüssel: + Pfad zum privaten SSH-Schlüssel START Stash Nicht-verfolgte Dateien einbeziehen Name: - Optional. Informationen zu diesem Stash + Optional. Name des Stashes. Modus: Nur gestagte Änderungen Gestagte und ungestagte Änderungen der ausgewählten Datei(en) werden gestasht!!! @@ -811,10 +813,10 @@ Anwenden Kopiere Nachricht Entfernen - Als Patch speichern... + Als Patch speichern… Stash entfernen Entfernen: - Stashes + STASHES ÄNDERUNGEN STASHES Statistiken @@ -827,14 +829,14 @@ Submodul hinzufügen BRANCH Branch - Relativen Pfad - De-initialisiere Submodul + Relativer Pfad + Deinitialisiere Submodul Untergeordnete Submodule fetchen Verlauf - Verschieben zu - Öffne Submodul Repository + Verschieben nach + Öffne Submodul-Repository Relativer Pfad: - Relativer Pfad um dieses Submodul zu speichern. + Relativer Pfad, um dieses Submodul zu speichern. Submodul löschen Branch setzen URL ändern @@ -843,24 +845,25 @@ nicht initialisiert Revision geändert nicht gemerged - Update + Aktualisieren URL OK TAGGER - TIME + ZEIT + Vergleiche 2 Tags + Vergleichen mit… + Vergleichen mit HEAD Nachricht Name Tagger - Name des Tags kopieren + Namen des Tags kopieren Benutzerdefinierte Aktion - Lösche ${0}$... - Selektierte {0} Tags löschen... - Merge ${0}$ in ${1}$ hinein... - Pushe ${0}$... + Lösche ${0}$… + Lösche ausgewählte {0} Tags… + Pushe ${0}$… Submodule aktualisieren Alle Submodule - Initialisiere wenn nötig - Rekursiv + Bei Bedarf initialisieren Submodul: Auf verfolgten Remote-Branch des Submoduls aktualisieren URL: @@ -877,55 +880,57 @@ DRAG & DROP VON ORDNER UNTERSTÜTZT. BENUTZERDEFINIERTE GRUPPIERUNG UNTERSTÜTZT. Bearbeiten Verschiebe in eine andere Gruppe - Öffne alle Repositories + Öffne alle Repositorys Öffne Repository Öffne Terminal - Standard Klon-Ordner erneut nach Repositories durchsuchen - Suche Repositories... - Änderungen + Standard Klon-Ordner erneut nach Repositorys durchsuchen + Suche Repositorys… + ÄNDERUNGEN Git Ignore Ignoriere alle *{0} Dateien Ignoriere *{0} Dateien im selben Ordner Ignoriere nicht-verfolgte Dateien in diesem Ordner Ignoriere nur diese Datei - Amend + Nachbesserung 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. + Soll wirklich der Verlauf aller Commit-Nachrichten gelöscht werden? Dies kann nicht rückgängig gemacht werden. COMMIT COMMIT & PUSH - Template/Historie + Vorlage / Historie Klick-Ereignis auslösen Commit (Bearbeitung) Alle Änderungen stagen und committen - Du erzeugst einen Commit auf einem losgelösten (detached) HEAD. Möchtest du trotzdem fortfahren? + Du erzeugst einen Commit auf einem losgelösten (‚detached‘) HEAD. Möchtest du trotzdem fortfahren? Du hast {0} Datei(en) gestaged, aber nur {1} werden angezeigt ({2} sind herausgefiltert). Möchtest du trotzdem fortfahren? KONFLIKTE ERKANNT - EXTERNES MERGE-TOOL ÖFFNEN + MERGE + Merge mit externem Tool ALLE KONFLIKTE IN EXTERNEM MERGE-TOOL ÖFFNEN - DATEI KONFLIKTE GELÖST + DATEI-KONFLIKTE SIND GELÖST MEINE VERSION VERWENDEN - IHRE VERSION VERWENDEN + DEREN VERSION VERWENDEN NICHT-VERFOLGTE DATEIEN EINBEZIEHEN KEINE BISHERIGEN COMMIT-NACHRICHTEN - KEINE COMMIT TEMPLATES + KEINE COMMIT-VORLAGEN Ohne Überprüfung Autor zurücksetzen - SignOff + Abzeichnen GESTAGED UNSTAGEN ALLES UNSTAGEN - UNSTAGED + UNGESTAGED STAGEN ALLES STAGEN ALS UNVERÄNDERT BETRACHTETE ANZEIGEN - Template: ${0}$ - ARBEITSPLATZ: - Arbeitsplätze konfigurieren... + Vorlage: ${0}$ + ARBEITSUMGEBUNG: + Arbeitsumgebungen konfigurieren… WORKTREE Pfad kopieren Sperren Öffnen Entfernen Entsperren + JA diff --git a/src/Resources/Locales/el_GR.axaml b/src/Resources/Locales/el_GR.axaml new file mode 100644 index 000000000..4de2a9003 --- /dev/null +++ b/src/Resources/Locales/el_GR.axaml @@ -0,0 +1,1038 @@ + + + + + + Σχετικά + Σχετικά με το SourceGit + Ημερομηνία έκδοσης: {0} + Σημειώσεις έκδοσης + Ελεύθερο & ανοιχτού κώδικα γραφικό περιβάλλον για το Git + Προσθήκη αρχείου(-ων) στα αγνοούμενα + Μοτίβο: + Αρχείο αποθήκευσης: + Προσθήκη Worktree + Τοποθεσία: + Διαδρομή για αυτό το worktree. Υποστηρίζεται σχετική διαδρομή. + Όνομα κλάδου: + Προαιρετικό. Προεπιλογή είναι το όνομα του φακέλου προορισμού. + Παρακολούθηση κλάδου: + Παρακολούθηση απομακρυσμένου κλάδου + Τι να γίνει checkout: + Δημιουργία νέου κλάδου + Υπάρχων κλάδος + Βοηθός AI + ΜΟΝΤΕΛΟ + ΕΠΑΝΑΔΗΜΙΟΥΡΓΙΑ + Χρήση AI για δημιουργία μηνύματος commit + Χρήση + Απόκρυψη SourceGit + Απόκρυψη άλλων + Εμφάνιση όλων + Τριμερής συγχώνευση (3-Way) + Επιλέξτε αρχείο .patch για εφαρμογή + Αγνόηση αλλαγών κενών χαρακτήρων + Πηγή: + Αρχείο + Πρόχειρο + Εφαρμογή Patch + Κενά: + Εφαρμογή Stash + Διαγραφή μετά την εφαρμογή + Επαναφορά των αλλαγών του index + Stash: + Αρχειοθέτηση... + Αποθήκευση αρχείου σε: + Επιλέξτε διαδρομή για το αρχείο + Αναθεώρηση: + Αρχειοθέτηση + SourceGit Askpass + Εισαγωγή κωδικής φράσης: + ΑΡΧΕΙΑ ΠΟΥ ΘΕΩΡΟΥΝΤΑΙ ΑΜΕΤΑΒΛΗΤΑ + ΚΑΝΕΝΑ ΑΡΧΕΙΟ ΔΕΝ ΘΕΩΡΕΙΤΑΙ ΑΜΕΤΑΒΛΗΤΟ + Φόρτωση εικόνας... + Ανανέωση + ΤΟ ΔΥΑΔΙΚΟ ΑΡΧΕΙΟ ΔΕΝ ΥΠΟΣΤΗΡΙΖΕΤΑΙ!!! + Bisect + Ματαίωση + Κακό + Καλό + Παράλειψη + Εκτέλεση bisect. Κάντε checkout σε άλλο και μετά σημειώστε το ως καλό ή κακό. + Εκτέλεση bisect. Σημειώστε το τρέχον commit ως κακό ή κάντε checkout σε άλλο και μετά σημειώστε το ως κακό. + Εκτέλεση bisect. Σημειώστε το τρέχον commit ως καλό ή κακό και κάντε checkout σε άλλο. + Εκτέλεση bisect. Το τρέχον HEAD είναι καλό ή κακό; + Blame + Blame στην προηγούμενη αναθεώρηση + Αγνόηση αλλαγών κενών χαρακτήρων + ΤΟ BLAME ΣΕ ΑΥΤΟ ΤΟ ΑΡΧΕΙΟ ΔΕΝ ΥΠΟΣΤΗΡΙΖΕΤΑΙ!!! + Checkout ${0}$... + Σύγκριση των 2 επιλεγμένων κλάδων + Σύγκριση με... + Σύγκριση με το HEAD + Σύγκριση με ${0}$ + Αντιγραφή ονόματος κλάδου + Δημιουργία PR... + Δημιουργία PR για το upstream ${0}$... + Προσαρμοσμένη ενέργεια + Διαγραφή ${0}$... + Διαγραφή {0} επιλεγμένων κλάδων + Επεξεργασία περιγραφής για ${0}$... + Fast-Forward στο ${0}$ + Fetch ${0}$ στο ${1}$... + Git Flow - Ολοκλήρωση ${0}$ + Διαδραστικό rebase του ${0}$ πάνω στο ${1}$ + Συγχώνευση ${0}$ στο ${1}$... + Συγχώνευση {0} επιλεγμένων κλάδων στον τρέχοντα + Pull ${0}$ + Pull ${0}$ στο ${1}$... + Push ${0}$ + Rebase του ${0}$ πάνω στο ${1}$... + Μετονομασία ${0}$... + Επαναφορά ${0}$ στο ${1}$... + Μετάβαση στο ${0}$ (worktree) + Ορισμός κλάδου παρακολούθησης... + {0} commit μπροστά + {0} commit μπροστά, {1} commit πίσω + {0} commit πίσω + Μη έγκυρο + ΑΠΟΜΑΚΡΥΣΜΕΝΟ + ΚΑΤΑΣΤΑΣΗ + ΠΑΡΑΚΟΛΟΥΘΗΣΗ + URL + WORKTREE + ΑΚΥΡΟ + Επαναφορά στη γονική αναθεώρηση + Επαναφορά σε αυτή την αναθεώρηση + Δημιουργία μηνύματος commit + Συγχώνευση (ενσωματωμένη) + Συγχώνευση (εξωτερική) + Επαναφορά αρχείου(-ων) στο ${0}$ + ΑΛΛΑΓΗ ΤΡΟΠΟΥ ΕΜΦΑΝΙΣΗΣ + Εμφάνιση ως λίστα αρχείων και φακέλων + Εμφάνιση ως λίστα διαδρομών + Εμφάνιση ως δέντρο αρχείων + Αλλαγή URL της υπομονάδας + Υπομονάδα: + URL: + Checkout κλάδου + Τοπικές αλλαγές: + Κλάδος: + Το τρέχον HEAD περιέχει commit που δεν συνδέονται με κανέναν κλάδο/ετικέτα! Θέλετε να συνεχίσετε; + Οι ακόλουθες υπομονάδες πρέπει να ενημερωθούν:{0}Θέλετε να τις ενημερώσετε; + Checkout & Fast-Forward + Fast-Forward στο: + Checkout κλάδου από Stash + Νέος κλάδος: + Stash: + Checkout commit ή ετικέτας + Στόχος: + Προσοχή: Μετά από αυτό, το HEAD θα αποσυνδεθεί (detached) + Cherry-Pick + Προσθήκη πηγής στο μήνυμα commit + Commit: + Commit όλων των αλλαγών + Κύρια γραμμή: + Συνήθως δεν μπορείτε να κάνετε cherry-pick μια συγχώνευση, επειδή δεν γνωρίζετε ποια πλευρά της συγχώνευσης πρέπει να θεωρηθεί η κύρια γραμμή. Αυτή η επιλογή επιτρέπει στο cherry-pick να αναπαράγει την αλλαγή σε σχέση με τον καθορισμένο γονέα. + Εκκαθάριση Stashes + Πρόκειται να εκκαθαρίσετε όλα τα stashes. Είστε σίγουροι ότι θέλετε να συνεχίσετε; + Κλωνοποίηση απομακρυσμένου αποθετηρίου + Επιπλέον παράμετροι: + Πρόσθετα ορίσματα για την κλωνοποίηση του αποθετηρίου. Προαιρετικό. + Σελιδοδείκτης: + Ομάδα: + Τοπικό όνομα: + Όνομα αποθετηρίου. Προαιρετικό. + Γονικός φάκελος: + Αρχικοποίηση & ενημέρωση υπομονάδων + URL αποθετηρίου: + ΚΛΕΙΣΙΜΟ + Επεξεργαστής + Κλάδοι + Κλάδοι & Ετικέτες + Προσαρμοσμένες ενέργειες αποθετηρίου + Αρχεία αναθεώρησης + Checkout commit + Cherry-Pick commit + Cherry-Pick ... + Σύγκριση με το HEAD + Σύγκριση με το Worktree + Συγγραφέας + Ώρα συγγραφέα + Μήνυμα + Committer + Ώρα committer + SHA + Θέμα + Προσαρμοσμένη ενέργεια + Διαδραστικό rebase + Απόρριψη (Drop)... + Επεξεργασία... + Fixup στον γονέα... + Διαδραστικό rebase του ${0}$ πάνω στο ${1}$ + Επαναδιατύπωση... + Squash στον γονέα... + Συγχώνευση ... + Push ${0}$ στο ${1}$ + Rebase του ${0}$ πάνω στο ${1}$ + Επαναφορά ${0}$ στο ${1}$ + Αναίρεση commit + Αποθήκευση ως Patch... + ΑΛΛΑΓΕΣ + αλλαγμένο(-α) αρχείο(-α) + Αναζήτηση αλλαγών... + Σύμπτυξη λεπτομερειών + ΑΡΧΕΙΑ + Αρχείο LFS + Αναζήτηση αρχείων... + Υπομονάδα + ΠΛΗΡΟΦΟΡΙΕΣ + ΣΥΓΓΡΑΦΕΑΣ + COMMITTER + Έλεγχος refs που περιέχουν αυτό το commit + ΤΟ COMMIT ΠΕΡΙΕΧΕΤΑΙ ΣΕ + Αντιγραφή email + Αντιγραφή ονόματος + Αντιγραφή ονόματος & email + Εμφανίζονται μόνο οι πρώτες 100 αλλαγές. Δείτε όλες τις αλλαγές στην καρτέλα ΑΛΛΑΓΕΣ. + Κλειδί: + ΜΗΝΥΜΑ + ΓΟΝΕΙΣ + REFS + SHA + Υπογράφων: + Άνοιγμα στον περιηγητή + ΣΤΗΛΗ + Εισαγάγετε το μήνυμα commit. Χρησιμοποιήστε μια κενή γραμμή για να διαχωρίσετε το θέμα από την περιγραφή! + ΘΕΜΑ + Σύγκριση + ΑΛΛΑΓΕΣ + COMMITS + COMMITS ΜΟΝΟ ΑΡΙΣΤΕΡΑ + COMMITS ΜΟΝΟ ΔΕΞΙΑ + Συμβουλή: Μπορείτε να κάνετε cherry-pick τα επιλεγμένα commits αν η άλλη πλευρά είναι το HEAD (μέσω του μενού περιβάλλοντος). + Ρύθμιση αποθετηρίου + ΠΡΟΤΥΠΟ COMMIT + Ενσωματωμένες παράμετροι: + + ${branch_name} Όνομα τρέχοντος τοπικού κλάδου. + ${files_num} Πλήθος αλλαγμένων αρχείων + ${files} Διαδρομές αλλαγμένων αρχείων + ${files:N} Έως N διαδρομές αλλαγμένων αρχείων + ${pure_files} Όπως το ${files}, αλλά μόνο καθαρά ονόματα αρχείων + ${pure_files:N} Όπως το ${files:N}, αλλά χωρίς φακέλους + Περιεχόμενο προτύπου: + Όνομα προτύπου: + ΠΡΟΣΑΡΜΟΣΜΕΝΗ ΕΝΕΡΓΕΙΑ + Ορίσματα: + Ενσωματωμένες παράμετροι: + + ${REPO} Διαδρομή του αποθετηρίου + ${REMOTE} Επιλεγμένο remote ή το remote του επιλεγμένου κλάδου + ${BRANCH} Επιλεγμένος κλάδος, χωρίς το τμήμα ${REMOTE} για απομακρυσμένους κλάδους + ${BRANCH_FRIENDLY_NAME} Φιλικό όνομα του επιλεγμένου κλάδου, περιέχει το τμήμα ${REMOTE} για απομακρυσμένους κλάδους + ${SHA} Hash του επιλεγμένου commit + ${TAG} Επιλεγμένη ετικέτα + ${FILE} Επιλεγμένο αρχείο, σε σχέση με τη ρίζα του αποθετηρίου + $1, $2 ... Τιμές των στοιχείων εισόδου + Εκτελέσιμο αρχείο: + Στοιχεία εισόδου: + Επεξεργασία + Όνομα: + Εμβέλεια: + Κλάδος + Commit + Αρχείο + Απομακρυσμένο + Αποθετήριο + Ετικέτα + Αναμονή για έξοδο της ενέργειας + Διεύθυνση email + Διεύθυνση email + GIT + Ερώτηση πριν την αυτόματη ενημέρωση υπομονάδων + Αυτόματο fetch από τα απομακρυσμένα + Λεπτό(-ά) + Τύποι Conventional Commit + Προεπιλεγμένο απομακρυσμένο + Ενεργοποίηση `--recursive` κατά την αυτόματη ενημέρωση υπομονάδων + Προτιμώμενος τρόπος συγχώνευσης + ΣΥΣΤΗΜΑ ΠΑΡΑΚΟΛΟΥΘΗΣΗΣ ΖΗΤΗΜΑΤΩΝ + Προσθήκη κανόνα Azure DevOps + Προσθήκη κανόνα Gerrit Change-Id Commit + Προσθήκη κανόνα Gitee Issue + Προσθήκη κανόνα Gitee Pull Request + Προσθήκη κανόνα GitHub + Προσθήκη κανόνα GitLab Issue + Προσθήκη κανόνα GitLab Merge Request + Προσθήκη κανόνα Jira + Νέος κανόνας + Κανονική έκφραση ζητήματος: + Όνομα κανόνα: + Κοινή χρήση αυτού του κανόνα στο αρχείο .issuetracker + URL αποτελέσματος: + Χρησιμοποιήστε $1, $2 για πρόσβαση στις τιμές των ομάδων της κανονικής έκφρασης. + AI + Προτιμώμενη υπηρεσία: + Αν οριστεί η «Προτιμώμενη υπηρεσία», το SourceGit θα τη χρησιμοποιεί μόνο σε αυτό το αποθετήριο. Διαφορετικά, αν υπάρχουν περισσότερες από μία διαθέσιμες υπηρεσίες, θα εμφανίζεται μενού για να επιλέξετε μία. + HTTP Proxy + HTTP proxy που χρησιμοποιεί αυτό το αποθετήριο + Όνομα χρήστη + Όνομα χρήστη για αυτό το αποθετήριο + Επεξεργασία στοιχείων προσαρμοσμένης ενέργειας + Τιμή όταν επιλεγεί: + Όταν είναι επιλεγμένο, αυτή η τιμή θα χρησιμοποιείται στα ορίσματα της γραμμής εντολών + Περιγραφή: + Προεπιλογή: + Είναι φάκελος: + Ετικέτα: + Επιλογές: + Χρησιμοποιήστε το '|' ως διαχωριστικό για τις επιλογές + Μορφοποιητής συμβολοσειράς: + Προαιρετικό. Χρησιμοποιείται για μορφοποίηση της συμβολοσειράς εξόδου. Αγνοείται όταν η είσοδος είναι κενή. Χρησιμοποιήστε το `${VALUE}` για να αναπαραστήσετε τη συμβολοσειρά εισόδου. + Οι ενσωματωμένες μεταβλητές ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, και ${TAG} παραμένουν διαθέσιμες εδώ + Τύπος: + Χρήση φιλικού ονόματος: + Χώροι εργασίας + Χρώμα + Όνομα + Επαναφορά καρτελών κατά την εκκίνηση + ΣΥΝΕΧΕΙΑ + Εντοπίστηκε κενό commit! Θέλετε να συνεχίσετε (--allow-empty); + ΠΡΟΣΘΗΚΗ ΟΛΩΝ & COMMIT + ΠΡΟΣΘΗΚΗ ΕΠΙΛΕΓΜΕΝΩΝ & COMMIT + Εντοπίστηκε κενό commit! Θέλετε να συνεχίσετε (--allow-empty) ή να γίνει αυτόματη προσθήκη στο ευρετήριο και μετά commit; + Απαιτείται επανεκκίνηση + Πρέπει να επανεκκινήσετε την εφαρμογή για να εφαρμοστούν οι αλλαγές. + Βοηθός Conventional Commit + Breaking Change: + Ζήτημα που κλείνει: + Λεπτομέρειες αλλαγών: + Εμβέλεια: + Σύντομη περιγραφή: + Τύπος αλλαγής: + Αντιγραφή + Αντιγραφή όλου του κειμένου + Αντιγραφή ως Patch + Αντιγραφή πλήρους διαδρομής + Αντιγραφή διαδρομής + Δημιουργία κλάδου... + Με βάση: + Checkout του κλάδου που δημιουργήθηκε + Τοπικές αλλαγές: + Όνομα νέου κλάδου: + Εισαγάγετε όνομα κλάδου. + Δημιουργία τοπικού κλάδου + Αντικατάσταση υπάρχοντος κλάδου + Δημιουργία ετικέτας... + Νέα ετικέτα στο: + Υπογραφή GPG + Μήνυμα ετικέτας: + Προαιρετικό. + Όνομα ετικέτας: + Προτεινόμενη μορφή: v1.0.0-alpha + Push σε όλα τα απομακρυσμένα μετά τη δημιουργία + Δημιουργία ετικέτας + Είδος: + επισημειωμένη + ελαφριά + Κρατήστε Ctrl για άμεση εκκίνηση + Αποκοπή + Απόρριψη + Καμία ενέργεια + Stash & επανεφαρμογή + Απο-αρχικοποίηση υπομονάδας + Εξαναγκασμός απο-αρχικοποίησης ακόμη κι αν περιέχει τοπικές αλλαγές. + Υπομονάδα: + Διαγραφή κλάδου + Θέλετε να διαγράψετε και τον ακόλουθο απομακρυσμένο κλάδο; + Κλάδος: + Εξαναγκασμένη διαγραφή ακόμη κι αν περιέχει μη συγχωνευμένα commit + Πρόκειται να διαγράψετε έναν απομακρυσμένο κλάδο!!! + Διαγραφή πολλαπλών κλάδων + Προσπαθείτε να διαγράψετε πολλούς κλάδους ταυτόχρονα. Βεβαιωθείτε ότι ελέγξατε ξανά πριν προχωρήσετε! + Διαγραφή πολλαπλών ετικετών + Διαγραφή τους από τα απομακρυσμένα + Προσπαθείτε να διαγράψετε πολλές ετικέτες ταυτόχρονα. Βεβαιωθείτε ότι ελέγξατε ξανά πριν προχωρήσετε! + Διαγραφή απομακρυσμένου + Απομακρυσμένο: + Διαδρομή: + Στόχος: + Όλα τα παιδιά θα αφαιρεθούν από τη λίστα. + Αυτό θα το αφαιρέσει μόνο από τη λίστα, όχι από τον δίσκο! + Επιβεβαίωση διαγραφής ομάδας + Επιβεβαίωση διαγραφής αποθετηρίου + Διαγραφή υπομονάδας + Διαδρομή υπομονάδας: + Διαγραφή ετικέτας + Ετικέτα: + Διαγραφή από τα απομακρυσμένα αποθετήρια + ΔΥΑΔΙΚΟ DIFF + ΚΕΝΟ ΑΡΧΕΙΟ + Πρώτη διαφορά + Αγνόηση αλλαγών κενών χαρακτήρων + ΑΝΑΜΕΙΞΗ + ΔΙΑΦΟΡΑ + ΔΙΠΛΑ-ΔΙΠΛΑ + ΣΑΡΩΣΗ + Τελευταία διαφορά + ΑΛΛΑΓΗ ΑΝΤΙΚΕΙΜΕΝΟΥ LFS + ΝΕΟ + Επόμενη διαφορά + ΚΑΜΙΑ ΑΛΛΑΓΗ Ή ΜΟΝΟ ΑΛΛΑΓΕΣ EOL + ΠΑΛΙΟ + Προηγούμενη διαφορά + Αποθήκευση ως Patch + Εμφάνιση κρυφών συμβόλων + Diff δίπλα-δίπλα + ΥΠΟΜΟΝΑΔΑ + ΔΙΑΓΡΑΦΗΚΕ + ΝΕΟ + + {0} μη υποβληθείσες αλλαγές + Εναλλαγή + Επισήμανση σύνταξης + Αναδίπλωση γραμμών + Άνοιγμα στο εργαλείο συγχώνευσης + Εμφάνιση όλων των γραμμών + Μείωση αριθμού ορατών γραμμών + Αύξηση αριθμού ορατών γραμμών + ΕΠΙΛΕΞΤΕ ΑΡΧΕΙΟ ΓΙΑ ΠΡΟΒΟΛΗ ΑΛΛΑΓΩΝ + Ιστορικό φακέλου + Έχει τοπικές αλλαγές + Αναντιστοιχία με το upstream + Ήδη ενημερωμένο + Απόρριψη αλλαγών + Όλες οι τοπικές αλλαγές στο αντίγραφο εργασίας. + Αλλαγές: + Συμπερίληψη αγνοούμενων αρχείων + Συμπερίληψη τροποποιημένων/διαγραμμένων αρχείων + Συμπερίληψη μη παρακολουθούμενων αρχείων + {0} αλλαγές θα απορριφθούν + Δεν μπορείτε να αναιρέσετε αυτή την ενέργεια!!! + Επεξεργασία περιγραφής κλάδου + Στόχος: + Σελιδοδείκτης: + Νέο όνομα: + Στόχος: + Επεξεργασία επιλεγμένης ομάδας + Επεξεργασία επιλεγμένου αποθετηρίου + Στόχος: + Αυτό το αποθετήριο + Fetch + Fetch από όλα τα απομακρυσμένα + Εξαναγκασμός αντικατάστασης τοπικών refs + Fetch χωρίς ετικέτες + Απομακρυσμένο: + Fetch απομακρυσμένων αλλαγών + Θεώρηση ως αμετάβλητο + Προσαρμοσμένη ενέργεια + Απόρριψη... + Απόρριψη {0} αρχείων... + Επίλυση με χρήση ${0}$ + Αποθήκευση ως Patch... + Προσθήκη στο ευρετήριο + Προσθήκη {0} αρχείων στο ευρετήριο + Stash... + Stash {0} αρχείων... + Αφαίρεση από το ευρετήριο + Αφαίρεση {0} αρχείων από το ευρετήριο + Χρήση δικού μου (checkout --ours) + Χρήση δικού τους (checkout --theirs) + Ιστορικό αρχείου + ΑΛΛΑΓΗ + ΠΕΡΙΕΧΟΜΕΝΟ + Αλλαγή mode αρχείου: + mode διαγραμμένου αρχείου: + Κατάλογος + Εκτελέσιμο + mode νέου αρχείου: + Κανονικό + Υπομονάδα + Συμβολικός σύνδεσμος + Άγνωστο + Git-Flow + Κλάδος ανάπτυξης: + Feature: + Πρόθεμα Feature: + Ολοκλήρωση ${0}$ + FLOW - Ολοκλήρωση Feature + FLOW - Ολοκλήρωση Hotfix + FLOW - Ολοκλήρωση Release + Στόχος: + Rebase πριν τη συγχώνευση + Squash κατά τη συγχώνευση + Hotfix: + Πρόθεμα Hotfix: + Αρχικοποίηση Git-Flow + Διατήρηση κλάδου + Κλάδος παραγωγής: + Release: + Πρόθεμα Release: + Έναρξη στο: + Έναρξη Feature... + FLOW - Έναρξη Feature + Έναρξη Hotfix... + FLOW - Έναρξη Hotfix + Όνομα: + Εισαγάγετε όνομα + Έναρξη Release... + FLOW - Έναρξη Release + Πρόθεμα ετικέτας έκδοσης: + Git LFS + Προσθήκη μοτίβου παρακολούθησης... + Το μοτίβο είναι όνομα αρχείου + Προσαρμοσμένο μοτίβο: + Προσθήκη μοτίβου παρακολούθησης στο Git LFS + Fetch + Εκτελέστε `git lfs fetch` για να κατεβάσετε αντικείμενα Git LFS. Αυτό δεν ενημερώνει το αντίγραφο εργασίας. + Fetch αντικειμένων LFS + Εγκατάσταση hooks του Git LFS + Εμφάνιση κλειδωμάτων + Δεν υπάρχουν κλειδωμένα αρχεία + Κλείδωμα + Εμφάνιση μόνο των δικών μου κλειδωμάτων + Κλειδώματα LFS + Ξεκλείδωμα + Ξεκλείδωμα όλων των δικών μου κλειδωμάτων + Είστε σίγουροι ότι θέλετε να ξεκλειδώσετε όλα τα κλειδωμένα σας αρχεία; + Εξαναγκασμός ξεκλειδώματος + Εκκαθάριση + Εκτελέστε `git lfs prune` για να διαγράψετε παλιά αρχεία LFS από την τοπική αποθήκευση + Pull + Εκτελέστε `git lfs pull` για να κατεβάσετε όλα τα αρχεία Git LFS για το τρέχον ref & checkout + Pull αντικειμένων LFS + Push + Push των μεγάλων αρχείων που βρίσκονται σε αναμονή προς το endpoint του Git LFS + Push αντικειμένων LFS + Απομακρυσμένο: + Παρακολούθηση αρχείων με όνομα '{0}' + Παρακολούθηση όλων των αρχείων *{0} + Επιλογή commit + ΙΣΤΟΡΙΚΟ + ΣΥΓΓΡΑΦΕΑΣ + ΩΡΑ ΣΥΓΓΡΑΦΕΑ + ΩΡΑ COMMIT + ΓΡΑΦΗΜΑ & ΘΕΜΑ + SHA + ΕΠΙΣΗΜΑΝΣΕΙΣ ΣΤΟ ΓΡΑΦΗΜΑ + Όλα + Μόνο ο τρέχων κλάδος + Τρέχων κλάδος & επιλεγμένα commits + Μόνο τα επιλεγμένα commits + ΕΠΙΛΕΓΜΕΝΑ {0} COMMITS + ΕΜΦΑΝΙΣΗ ΣΤΗΛΩΝ + Κρατήστε 'Ctrl' ή 'Shift' για να επιλέξετε πολλαπλά commits. + Κρατήστε ⌘ ή ⇧ για να επιλέξετε πολλαπλά commits. + ΣΥΜΒΟΥΛΕΣ: + Άνοιγμα σε ξεχωριστό παράθυρο + Λεπτομέρειες commit + Σύγκριση αναθεωρήσεων + Αναφορά συντομεύσεων πληκτρολογίου + ΚΑΘΟΛΙΚΑ + Κλωνοποίηση νέου αποθετηρίου + Κλείσιμο τρέχουσας καρτέλας + Μετάβαση στην επόμενη καρτέλα + Μετάβαση στην προηγούμενη καρτέλα + Δημιουργία νέας καρτέλας + Άνοιγμα τοπικού αποθετηρίου + Άνοιγμα διαλόγου Προτιμήσεων + Εμφάνιση αναπτυσσόμενου μενού χώρων εργασίας + Εναλλαγή ενεργής καρτέλας + Μεγέθυνση/σμίκρυνση + ΑΠΟΘΕΤΗΡΙΟ + Commit των αλλαγών στο ευρετήριο + Commit και push των αλλαγών στο ευρετήριο + Προσθήκη όλων των αλλαγών στο ευρετήριο και commit + Δημιουργία νέου κλάδου + Fetch, ξεκινά άμεσα + Λειτουργία πίνακα ελέγχου (Προεπιλογή) + Μετάβαση στον απόγονο του επιλεγμένου commit + Μετάβαση στον γονέα του επιλεγμένου commit + Άνοιγμα παλέτας εντολών + Λειτουργία αναζήτησης commit + Pull, ξεκινά άμεσα + Push, ξεκινά άμεσα + Εξαναγκασμός επαναφόρτωσης αυτού του αποθετηρίου + Ανάπτυξη/σύμπτυξη πίνακα λεπτομερειών στο `ΙΣΤΟΡΙΚΟ` + Μετάβαση στις 'ΤΟΠΙΚΕΣ ΑΛΛΑΓΕΣ' + Μετάβαση στο 'ΙΣΤΟΡΙΚΟ' + Μετάβαση στα 'STASHES' + Απόρριψη + Προσθήκη στο ευρετήριο + Αφαίρεση από το ευρετήριο + Αρχικοποίηση αποθετηρίου + Θέλετε να εκτελέσετε την εντολή `git init` σε αυτή τη διαδρομή; + Το άνοιγμα του αποθετηρίου απέτυχε. Αιτία: + Διαδρομή: + Cherry-Pick σε εξέλιξη. + Επεξεργασία commit + Συγχώνευση σε εξέλιξη. + Συγχώνευση + Rebase σε εξέλιξη. + Σταμάτησε στο + Αναίρεση σε εξέλιξη. + Αναίρεση commit + Διαδραστικό rebase + Stash & επανεφαρμογή τοπικών αλλαγών + Παράκαμψη του pre-rebase hook + Πάνω στο: + Σύρετε και αφήστε για αναδιάταξη των commits + Κλάδος-στόχος: + Αντιγραφή συνδέσμου + Άνοιγμα στον περιηγητή + Εντολές + ΣΦΑΛΜΑ + ΕΙΔΟΠΟΙΗΣΗ + Άνοιγμα αποθετηρίων + Καρτέλες + Χώροι εργασίας + Συγχώνευση κλάδου + Προσαρμογή μηνύματος συγχώνευσης + Στο: + Τρόπος συγχώνευσης: + Πηγή: + Έλεγχος για συγχώνευση... + Η συγχώνευση μπορεί να γίνει χωρίς συγκρούσεις + Παρουσιάστηκε άγνωστο σφάλμα κατά τον έλεγχο για συγχώνευση + Η συγχώνευση θα προκαλέσει συγκρούσεις + Πρώτα το δικό μου, μετά το δικό τους + Πρώτα το δικό τους, μετά το δικό μου + ΧΡΗΣΗ ΚΑΙ ΤΩΝ ΔΥΟ + Όλες οι συγκρούσεις επιλύθηκαν + Απομένουν {0} συγκρούσεις + ΔΙΚΟ ΜΟΥ + Επόμενη σύγκρουση + Προηγούμενη σύγκρουση + ΑΠΟΤΕΛΕΣΜΑ + ΑΠΟΘΗΚΕΥΣΗ & ΠΡΟΣΘΗΚΗ + ΔΙΚΟ ΤΟΥΣ + Συγκρούσεις συγχώνευσης + Απόρριψη μη αποθηκευμένων αλλαγών; + ΧΡΗΣΗ ΔΙΚΟΥ ΜΟΥ + ΧΡΗΣΗ ΔΙΚΟΥ ΤΟΥΣ + ΑΝΑΙΡΕΣΗ + Συγχώνευση (πολλαπλή) + Commit όλων των αλλαγών + Στρατηγική: + Στόχοι: + Μετακίνηση υπομονάδας + Μετακίνηση σε: + Υπομονάδα: + Μετακίνηση κόμβου αποθετηρίου + Επιλέξτε γονικό κόμβο για: + Όνομα: + ΟΧΙ + Το Git ΔΕΝ έχει ρυθμιστεί. Μεταβείτε στις [Προτιμήσεις] και ρυθμίστε το πρώτα. + Άνοιγμα + Προεπιλεγμένος επεξεργαστής (συστήματος) + Άνοιγμα φακέλου αποθήκευσης δεδομένων + Άνοιγμα αρχείου + Άνοιγμα σε εξωτερικό εργαλείο συγχώνευσης + Άνοιγμα τοπικού αποθετηρίου + Σελιδοδείκτης: + Ομάδα: + Φάκελος: + Προαιρετικό. + Δημιουργία νέας καρτέλας + Κλείσιμο καρτέλας + Κλείσιμο άλλων καρτελών + Κλείσιμο καρτελών στα δεξιά + Αντιγραφή διαδρομής αποθετηρίου + Επεξεργασία + Μετακίνηση σε χώρο εργασίας + Ανανέωση + Αποθετήρια + Επικόλληση + {0} ημέρες πριν + 1 ώρα πριν + {0} ώρες πριν + Μόλις τώρα + Τον προηγούμενο μήνα + Πέρυσι + {0} λεπτά πριν + {0} μήνες πριν + {0} χρόνια πριν + Χθες + Προτιμήσεις + AI + Επιπλέον prompt (χρησιμοποιήστε `-` για να απαριθμήσετε τις απαιτήσεις σας) + Κλειδί API + Μοντέλο + Αυτόματη ανάκτηση διαθέσιμων model-ids + Όνομα + Η τιμή που εισήχθη είναι το όνομα για φόρτωση του κλειδιού API από το ENV + Διακομιστής + ΕΜΦΑΝΙΣΗ + Προεπιλεγμένη γραμματοσειρά + Πλάτος tab επεξεργαστή + Μέγεθος γραμματοσειράς + Προεπιλογή + Επεξεργαστής + Γραμματοσειρά σταθερού πλάτους + Θέμα + Παρακάμψεις θέματος + Χρήση μπαρών κύλισης αυτόματης απόκρυψης + Χρήση σταθερού πλάτους καρτελών στη γραμμή τίτλου + Χρήση εγγενούς πλαισίου παραθύρου + ΕΡΓΑΛΕΙΟ DIFF/MERGE + Ορίσματα Diff + Διαθέσιμες μεταβλητές: $LOCAL, $REMOTE + Ορίσματα Merge + Διαθέσιμες μεταβλητές: $BASE, $LOCAL, $REMOTE, $MERGED + Διαδρομή εγκατάστασης + Εισαγάγετε τη διαδρομή του εργαλείου diff/merge + Εργαλείο + ΓΕΝΙΚΑ + Έλεγχος για ενημερώσεις κατά την εκκίνηση + Μορφή ημερομηνίας + Ενεργοποίηση συμπαγών φακέλων στο δέντρο αλλαγών + Γλώσσα + Commits ιστορικού + Εμφάνιση της σελίδας `ΤΟΠΙΚΕΣ ΑΛΛΑΓΕΣ` από προεπιλογή + Εμφάνιση της καρτέλας `ΑΛΛΑΓΕΣ` στις λεπτομέρειες commit από προεπιλογή + Εμφάνιση σχετικού χρόνου στο γράφημα commit + Εμφάνιση ετικετών στο γράφημα commit + Μήκος οδηγού θέματος + 24ωρη μορφή + Χρήση συμπαγών ονομάτων κλάδων στο γράφημα commit + Δημιουργία προεπιλεγμένου avatar τύπου Github + GIT + Ενεργοποίηση αυτόματου CRLF + Προεπιλεγμένος φάκελος κλωνοποίησης + Email χρήστη + Καθολικό email χρήστη git + Εκκαθάριση νεκρών κλάδων μετά το fetch + Αγνόηση CR στο τέλος γραμμής στο diff κειμένου + Απαιτείται Git (>= 2.25.1) από αυτή την εφαρμογή + Διαδρομή εγκατάστασης + Ενεργοποίηση επαλήθευσης HTTP SSL + Χρήση git-credential-libsecret αντί για git-credential-manager + Όνομα χρήστη + Καθολικό όνομα χρήστη git + Stash & επανεφαρμογή αλλαγών από προεπιλογή κατά το checkout ή τη συγχώνευση κλάδων + Έκδοση Git + ΥΠΟΓΡΑΦΗ GPG + Υπογραφή GPG στα commit + Μορφή GPG + Διαδρομή εγκατάστασης προγράμματος + Εισαγάγετε τη διαδρομή του εγκατεστημένου προγράμματος gpg + Υπογραφή GPG στις ετικέτες + Κλειδί υπογραφής χρήστη + Κλειδί υπογραφής gpg του χρήστη + ΕΝΣΩΜΑΤΩΣΗ + SHELL/ΤΕΡΜΑΤΙΚΟ + Ορίσματα + Χρησιμοποιήστε το '.' για να υποδείξετε τον φάκελο εργασίας + Διαδρομή + Shell/Τερματικό + Εκκαθάριση απομακρυσμένου + Στόχος: + Εκκαθάριση Worktrees + Εκκαθάριση πληροφοριών worktree στο `$GIT_COMMON_DIR/worktrees` + Pull + Απομακρυσμένος κλάδος: + Στο: + Τοπικές αλλαγές: + Απομακρυσμένο: + Pull (Fetch & Merge) + Χρήση rebase αντί για συγχώνευση + Push + Βεβαιωθείτε ότι έχει γίνει push στις υπομονάδες + Εξαναγκασμένο push + Τοπικός κλάδος: + ΝΕΟ + Απομακρυσμένο: + Αναθεώρηση: + Push αναθεώρησης σε απομακρυσμένο + Push αλλαγών σε απομακρυσμένο + Απομακρυσμένος κλάδος: + Ορισμός ως κλάδου παρακολούθησης + Push όλων των ετικετών + Push ετικέτας σε απομακρυσμένο + Push σε όλα τα απομακρυσμένα + Απομακρυσμένο: + Ετικέτα: + Push σε ΝΕΟ κλάδο + Εισαγάγετε το όνομα του νέου απομακρυσμένου κλάδου: + Έξοδος + Rebase τρέχοντος κλάδου + Stash & επανεφαρμογή τοπικών αλλαγών + Παράκαμψη του pre-rebase hook + Πάνω στο: + Έλεγχος για rebase ... + Το rebase μπορεί να γίνει χωρίς συγκρούσεις + Παρουσιάστηκε άγνωστο σφάλμα κατά τον έλεγχο για rebase + Το rebase θα προκαλέσει συγκρούσεις + Προσθήκη απομακρυσμένου + Επεξεργασία απομακρυσμένου + Όνομα: + Όνομα απομακρυσμένου + URL αποθετηρίου: + URL απομακρυσμένου αποθετηρίου git + Αντιγραφή URL + Προσαρμοσμένη ενέργεια + Διαγραφή... + Επεξεργασία... + Ενεργοποίηση αυτόματου Fetch + Fetch + Άνοιγμα στον περιηγητή + Εκκαθάριση + Επιβεβαίωση αφαίρεσης Worktree + Ενεργοποίηση της επιλογής `--force` + Στόχος: + Μετονομασία κλάδου + Νέο όνομα: + Μοναδικό όνομα για αυτόν τον κλάδο + Κλάδος: + ΜΑΤΑΙΩΣΗ + Αυτόματο fetch αλλαγών από τα απομακρυσμένα... + Ταξινόμηση + Κατά ημερομηνία committer + Κατά όνομα + Εκκαθάριση (GC & Prune) + Εκτέλεση της εντολής `git gc` για αυτό το αποθετήριο. + Εκκαθάριση όλων + Εκκαθάριση + Ρύθμιση αυτού του αποθετηρίου + ΣΥΝΕΧΕΙΑ + Προσαρμοσμένες ενέργειες + Καμία προσαρμοσμένη ενέργεια + Πίνακας ελέγχου + Απόρριψη όλων των αλλαγών + Άνοιγμα στον περιηγητή αρχείων + Αναζήτηση κλάδων/ετικετών/υπομονάδων + Ορατότητα στο γράφημα + Μη ορισμένο + Απόκρυψη στο γράφημα commit + Φιλτράρισμα στο γράφημα commit + ΔΙΑΤΑΞΗ + Οριζόντια + Κάθετη + ΣΕΙΡΑ COMMITS + Ημερομηνία commit + Τοπολογικά + ΤΟΠΙΚΟΙ ΚΛΑΔΟΙ + Περισσότερες επιλογές... + Μετάβαση στο HEAD + Δημιουργία κλάδου + ΕΚΚΑΘΑΡΙΣΗ ΕΙΔΟΠΟΙΗΣΕΩΝ + Άνοιγμα ως φάκελος + Άνοιγμα σε {0} + Άνοιγμα σε εξωτερικά εργαλεία + ΑΠΟΜΑΚΡΥΣΜΕΝΑ + Προσθήκη απομακρυσμένου + ΕΠΙΛΥΣΗ + Αναζήτηση commit + Συγγραφέας + Περιεχόμενο + Μήνυμα + Διαδρομή + SHA + Τρέχων κλάδος + Μόνο commits με αναφορές + Μόνο πρώτος γονέας + ΕΜΦΑΝΙΣΗ ΣΗΜΑΙΩΝ + Εμφάνιση χαμένων commits + Εμφάνιση υπομονάδων ως δέντρο + Εμφάνιση ετικετών ως δέντρο + ΠΑΡΑΛΕΙΨΗ + Στατιστικά + ΥΠΟΜΟΝΑΔΕΣ + Προσθήκη υπομονάδας + Ενημέρωση υπομονάδας + ΕΤΙΚΕΤΕΣ + Νέα ετικέτα + Κατά ημερομηνία δημιουργίας + Κατά όνομα + Ταξινόμηση + Άνοιγμα στο τερματικό + Προβολή καταγραφών + Επίσκεψη '{0}' στον περιηγητή + WORKTREES + Προσθήκη Worktree + Εκκαθάριση + URL αποθετηρίου Git + Επαναφορά τρέχοντος κλάδου σε αναθεώρηση + Τρόπος επαναφοράς: + Μετακίνηση σε: + Τρέχων κλάδος: + Επαναφορά κλάδου (χωρίς checkout) + Μετακίνηση σε: + Κλάδος: + Εμφάνιση στον περιηγητή αρχείων + Αναίρεση commit + Commit: + Commit των αλλαγών αναίρεσης + Εκτελείται. Παρακαλώ περιμένετε... + ΑΠΟΘΗΚΕΥΣΗ + Αποθήκευση ως... + Το patch αποθηκεύτηκε με επιτυχία! + Σάρωση αποθετηρίων + Ριζικός φάκελος: + Σάρωση άλλου προσαρμοσμένου φακέλου + Έλεγχος για ενημερώσεις... + Είναι διαθέσιμη νέα έκδοση αυτού του λογισμικού: + Τρέχουσα έκδοση: + Ο έλεγχος για ενημερώσεις απέτυχε! + Λήψη + Παράλειψη αυτής της έκδοσης + Ημερομηνία έκδοσης νέας έκδοσης: + Ενημέρωση λογισμικού + Δεν υπάρχουν διαθέσιμες ενημερώσεις αυτή τη στιγμή. + Ορισμός κλάδου υπομονάδας + Υπομονάδα: + Τρέχων: + Αλλαγή σε: + Προαιρετικό. Όταν είναι κενό, ορίζεται στην προεπιλογή. + Ορισμός κλάδου παρακολούθησης + Κλάδος: + Κατάργηση upstream + Upstream: + Αντιγραφή SHA + Μετάβαση σε + Ιδιωτικό κλειδί SSH: + Διαδρομή αποθήκευσης ιδιωτικού κλειδιού SSH + ΕΝΑΡΞΗ + Stash + Συμπερίληψη μη παρακολουθούμενων αρχείων + Μήνυμα: + Προαιρετικό. Μήνυμα αυτού του stash + Λειτουργία: + Μόνο τα αρχεία στο ευρετήριο + Θα γίνουν stash τόσο οι αλλαγές στο ευρετήριο όσο και αυτές εκτός ευρετηρίου των επιλεγμένων αρχείων!!! + Stash τοπικών αλλαγών + Εφαρμογή + Εφαρμογή αλλαγών + Checkout νέου κλάδου + Αντιγραφή μηνύματος + Απόρριψη + Αποθήκευση ως Patch... + Απόρριψη Stash + Απόρριψη: + STASHES + ΑΛΛΑΓΕΣ + STASHES + Στατιστικά + ΕΠΙΣΚΟΠΗΣΗ + ΜΗΝΑΣ + ΕΒΔΟΜΑΔΑ + ΣΥΓΓΡΑΦΕΙΣ: + COMMITS: + ΥΠΟΜΟΝΑΔΕΣ + Προσθήκη υπομονάδας + ΚΛΑΔΟΣ + Κλάδος + Σχετική διαδρομή + Απο-αρχικοποίηση + Fetch ένθετων υπομονάδων + Ιστορικό + Μετακίνηση... + Άνοιγμα αποθετηρίου + Σχετική διαδρομή: + Σχετικός φάκελος για την αποθήκευση αυτής της υπομονάδας. + Διαγραφή + Ορισμός κλάδου + Αλλαγή URL + ΚΑΤΑΣΤΑΣΗ + τροποποιημένη + μη αρχικοποιημένη + η αναθεώρηση άλλαξε + μη συγχωνευμένη + Ενημέρωση + URL + Λεπτομέρειες αλλαγής υπομονάδας + ΑΝΟΙΓΜΑ ΛΕΠΤΟΜΕΡΕΙΩΝ + ΟΚ + ΔΗΜΙΟΥΡΓΟΣ ΕΤΙΚΕΤΑΣ + ΩΡΑ + Checkout ετικέτας... + Σύγκριση 2 ετικετών + Σύγκριση με... + Σύγκριση με το HEAD + Μήνυμα + Όνομα + Δημιουργός ετικέτας + Αντιγραφή ονόματος ετικέτας + Προσαρμοσμένη ενέργεια + Διαγραφή ${0}$... + Διαγραφή {0} επιλεγμένων ετικετών... + Συγχώνευση ${0}$ στο ${1}$... + Push ${0}$... + Ενημέρωση υπομονάδων + Όλες οι υπομονάδες + Αρχικοποίηση όπου χρειάζεται + Υπομονάδα: + Ενημέρωση ένθετων υπομονάδων + Ενημέρωση στον απομακρυσμένο κλάδο παρακολούθησης της υπομονάδας + URL: + Καταγραφές + ΕΚΚΑΘΑΡΙΣΗ ΟΛΩΝ + Αντιγραφή + Διαγραφή + Προειδοποίηση + Σελίδα υποδοχής + Δημιουργία ομάδας + Δημιουργία υπο-ομάδας + Κλωνοποίηση αποθετηρίου + Διαγραφή + ΥΠΟΣΤΗΡΙΖΕΤΑΙ ΜΕΤΑΦΟΡΑ & ΑΠΟΘΕΣΗ ΦΑΚΕΛΟΥ. ΥΠΟΣΤΗΡΙΖΕΤΑΙ ΠΡΟΣΑΡΜΟΣΜΕΝΗ ΟΜΑΔΟΠΟΙΗΣΗ. + Επεξεργασία + Μετακίνηση σε άλλη ομάδα + Άνοιγμα όλων των αποθετηρίων + Άνοιγμα αποθετηρίου + Άνοιγμα τερματικού + Επανασάρωση αποθετηρίων στον προεπιλεγμένο φάκελο κλωνοποίησης + Αναζήτηση αποθετηρίων... + ΤΟΠΙΚΕΣ ΑΛΛΑΓΕΣ + Git Ignore + Αγνόηση όλων των αρχείων *{0} + Αγνόηση αρχείων *{0} στον ίδιο φάκελο + Αγνόηση μη παρακολουθούμενων αρχείων σε αυτόν τον φάκελο + Αγνόηση μόνο αυτού του αρχείου + Αγνόηση όλων των μη παρακολουθούμενων αρχείων στον ίδιο φάκελο + Τροποποίηση + Μπορείτε να προσθέσετε αυτό το αρχείο στο ευρετήριο τώρα. + Εκκαθάριση ιστορικού + Είστε σίγουροι ότι θέλετε να εκκαθαρίσετε όλο το ιστορικό μηνυμάτων commit; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. + COMMIT + COMMIT & PUSH + Πρότυπο/Ιστορικό + Ενεργοποίηση συμβάντος κλικ + Commit (Επεξεργασία) + Προσθήκη όλων των αλλαγών στο ευρετήριο και commit + Δημιουργείτε commit σε αποσυνδεδεμένο (detached) HEAD. Θέλετε να συνεχίσετε; + Έχετε προσθέσει στο ευρετήριο {0} αρχείο(-α) αλλά εμφανίζονται μόνο {1} αρχείο(-α) ({2} αρχεία είναι φιλτραρισμένα). Θέλετε να συνεχίσετε; + ΕΝΤΟΠΙΣΤΗΚΑΝ ΣΥΓΚΡΟΥΣΕΙΣ + ΣΥΓΧΩΝΕΥΣΗ + Συγχώνευση με εξωτερικό εργαλείο + ΑΝΟΙΓΜΑ ΟΛΩΝ ΤΩΝ ΣΥΓΚΡΟΥΣΕΩΝ ΣΕ ΕΞΩΤΕΡΙΚΟ ΕΡΓΑΛΕΙΟ ΣΥΓΧΩΝΕΥΣΗΣ + ΟΙ ΣΥΓΚΡΟΥΣΕΙΣ ΑΡΧΕΙΩΝ ΕΠΙΛΥΘΗΚΑΝ + ΧΡΗΣΗ ΔΙΚΟΥ ΜΟΥ + ΧΡΗΣΗ ΔΙΚΟΥ ΤΟΥΣ + ΣΥΜΠΕΡΙΛΗΨΗ ΜΗ ΠΑΡΑΚΟΛΟΥΘΟΥΜΕΝΩΝ ΑΡΧΕΙΩΝ + ΔΕΝ ΥΠΑΡΧΟΥΝ ΠΡΟΣΦΑΤΑ ΜΗΝΥΜΑΤΑ ΕΙΣΟΔΟΥ + ΔΕΝ ΥΠΑΡΧΟΥΝ ΠΡΟΤΥΠΑ COMMIT + Χωρίς έλεγχο + Επαναφορά συγγραφέα + Προσυπογραφή + ΣΤΟ ΕΥΡΕΤΗΡΙΟ + ΑΦΑΙΡΕΣΗ + ΑΦΑΙΡΕΣΗ ΟΛΩΝ + ΕΚΤΟΣ ΕΥΡΕΤΗΡΙΟΥ + ΠΡΟΣΘΗΚΗ + ΠΡΟΣΘΗΚΗ ΟΛΩΝ + ΠΡΟΒΟΛΗ ΑΡΧΕΙΩΝ ΠΟΥ ΘΕΩΡΟΥΝΤΑΙ ΑΜΕΤΑΒΛΗΤΑ + Πρότυπο: ${0}$ + ΧΩΡΟΣ ΕΡΓΑΣΙΑΣ: + Ρύθμιση χώρων εργασίας... + WORKTREE + ΚΛΑΔΟΣ + Αντιγραφή διαδρομής + HEAD + Κλείδωμα + Άνοιγμα + ΔΙΑΔΡΟΜΗ + Αφαίρεση + Ξεκλείδωμα + ΝΑΙ + diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index daa23e13d..676bd57a0 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1,6 +1,7 @@ About About SourceGit + Release Date: {0} Release Notes Opensource & Free Git GUI Client Add File(s) To Ignore @@ -17,15 +18,19 @@ Create New Branch Existing Branch AI Assistant + MODEL RE-GENERATE Use AI to generate commit message - APPLY AS COMMIT MESSAGE + Use Hide SourceGit + Hide Others Show All - Patch - Patch File: + 3-Way Merge Select .patch file to apply Ignore whitespace changes + Source: + File + Clipboard Apply Patch Whitespace: Apply Stash @@ -47,37 +52,42 @@ Bisect Abort Bad - Bisecting. Is current HEAD good or bad? Good Skip - Bisecting. Mark current commit as good or bad and checkout another one. + Bisecting. Please checkout another one then mark it as good or bad. + Bisecting. Mark current commit as bad or checkout another one then mark it as bad. + Bisecting. Mark current commit as good or bad and checkout another one. + Bisecting. Is current HEAD good or bad? Blame Blame on Previous Revision + Ignore whitespace changes BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout ${0}$... - Compare with ${0}$ - Compare with Worktree + Compare selected 2 branches + Compare with... + Compare with HEAD + Compare with ${0}$ Copy Branch Name Create PR... Create PR for upstream ${0}$... Custom Action Delete ${0}$... - Delete selected {0} branches + Delete selected {0} branches... Edit description for ${0}$... Fast-Forward to ${0}$ - Fetch ${0}$ into ${1}$... - Git Flow - Finish ${0}$ + Fetch ${0}$ into ${1}$ + Git Flow - Finish ${0}$... + Interactively Rebase ${0}$ on ${1}$ Merge ${0}$ into ${1}$... - Merge selected {0} branches into current - Pull ${0}$ + Merge selected {0} branches into current... + Pull ${0}$... Pull ${0}$ into ${1}$... - Push ${0}$ + Push ${0}$... Rebase ${0}$ on ${1}$... Rename ${0}$... Reset ${0}$ to ${1}$... Switch to ${0}$ (worktree) Set Tracking Branch... - Branch Compare {0} commit(s) ahead {0} commit(s) ahead, {1} commit(s) behind {0} commit(s) behind @@ -91,6 +101,9 @@ Reset to Parent Revision Reset to This Revision Generate commit message + Merge (Built-in) + Merge (External) + Reset File(s) to ${0}$ CHANGE DISPLAY MODE Show as File and Dir List Show as Path List @@ -99,17 +112,18 @@ Submodule: URL: Checkout Branch - Checkout Commit - Commit: - Warning: By doing a commit checkout, your Head will be detached Local Changes: - Discard - Stash & Reapply - Update all submodules Branch: Your current HEAD contains commit(s) not connected to any branches/tags! Do you want to continue? + The following submodules need to be updated:{0}Do you want to update them? Checkout & Fast-Forward Fast-Forward to: + Checkout Branch From Stash + New Branch: + Stash: + Checkout Commit or Tag + Target: + Warning: After doing this, HEAD will be detached Cherry Pick Append source to commit message Commit(s): @@ -121,6 +135,8 @@ Clone Remote Repository Extra Parameters: Additional arguments to clone repository. Optional. + Bookmark: + Group: Local Name: Repository name. Optional. Parent Folder: @@ -128,18 +144,23 @@ Repository URL: CLOSE Editor - Checkout Commit - Cherry-Pick Commit - Cherry-Pick ... + Branches + Branches & Tags + Repository Custom Actions + Revision Files + Checkout Commit... + Cherry-Pick Commit... + Cherry-Pick... Compare with HEAD Compare with Worktree Author + Author Time Message Committer + Committer Time SHA Subject Custom Action - Drop Commit Interactive Rebase Drop... Edit... @@ -147,26 +168,22 @@ Interactively Rebase ${0}$ on ${1}$ Reword... Squash into Parent... - Merge to ${0}$ - Merge ... - Push ${0}$ to ${1}$ - Rebase ${0}$ on ${1}$ - Reset ${0}$ to ${1}$ - Revert Commit - Reword + Merge... + Push ${0}$ to ${1}$... + Rebase ${0}$ on ${1}$... + Reset ${0}$ to ${1}$... + Revert Commit... Save as Patch... - Squash into Parent - Fixup into Parent CHANGES changed file(s) Search Changes... + Collapse details FILES LFS File Search Files... Submodule INFORMATION AUTHOR - CHILDREN COMMITTER Check refs that contains this commit COMMIT IS CONTAINED BY @@ -181,12 +198,19 @@ SHA Signer: Open in Browser + COL Enter commit message. Please use an empty-line to separate subject and description! SUBJECT + Compare + CHANGES + COMMITS + LEFT ONLY COMMITS + RIGHT ONLY COMMITS + Tips: You can cherry-pick selected commits if the other side is HEAD (using context menu). Repository Configure COMMIT TEMPLATE - Built-in parameters: - + Built-in parameters: + ${branch_name} Current local branch name. ${files_num} Number of changed files ${files} Paths of changed files @@ -197,8 +221,8 @@ Template Name: CUSTOM ACTION Arguments: - Built-in parameters: - + Built-in parameters: + ${REPO} Repository's path ${REMOTE} Selected remote or selected branch's remote ${BRANCH} Selected branch, without ${REMOTE} part for remote branches @@ -222,10 +246,12 @@ Email Address Email address GIT + Ask before auto-updating submodules Fetch remotes automatically Minute(s) Conventional Commit Types Default Remote + Enable `--recursive` when auto-updating submodules Preferred Merge Mode ISSUE TRACKER Add Azure DevOps Rule @@ -258,8 +284,11 @@ Label: Options: Use '|' as delimiter for options + String Formatter: + Optional. Used to format output string. Ignored when input is empty. Please use `${VALUE}` to represent the input string. The built-in variables ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, and ${TAG} remain available here Type: + Use Friendly Name: Workspaces Color Name @@ -267,7 +296,8 @@ CONTINUE Empty commit detected! Do you want to continue (--allow-empty)? STAGE ALL & COMMIT - Empty commit detected! Do you want to continue (--allow-empty) or stage all then commit? + STAGE SELECTED & COMMIT + Empty commit detected! Do you want to continue (--allow-empty) or auto-stage then commit? Restart Required You need to restart this app to apply changes. Conventional Commit Helper @@ -279,14 +309,13 @@ Type of Change: Copy Copy All Text + Copy as Patch Copy Full Path Copy Path Create Branch... Based On: Check out the created branch Local Changes: - Discard - Stash & Reapply New Branch Name: Enter branch name. Create Local Branch @@ -299,19 +328,23 @@ Tag Name: Recommended format: v1.0.0-alpha Push to all remotes after created - Create New Tag + Create Tag Kind: annotated lightweight Hold Ctrl to start directly Cut + Discard + Do Nothing + Stash & Reapply De-initialize Submodule Force de-init even if it contains local changes. Submodule: Delete Branch + Do you want to also delete the following remote branch? Branch: + Force delete even if it contains unmerged commit(s) You are about to delete a remote branch!!! - 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 @@ -331,7 +364,7 @@ Tag: Delete from remote repositories BINARY DIFF - File Mode Changed + EMPTY FILE First Difference Ignore Whitespace Changes BLEND @@ -351,6 +384,7 @@ SUBMODULE DELETED NEW + + {0} Uncommitted Changes Swap Syntax Highlighting Line Word Wrap @@ -367,12 +401,10 @@ All local changes in working copy. Changes: Include ignored files + Include modified/deleted files 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: @@ -405,15 +437,25 @@ File History CHANGE CONTENT + File mode change: + Deleted file mode: + Directory + Executable + New file mode: + Normal + Submodule + Symlink + Unknown Git-Flow Development Branch: Feature: Feature Prefix: - FLOW - Finish Feature - FLOW - Finish Hotfix - FLOW - Finish Release + Finish ${0}$ + Finish Feature + Finish Hotfix + Finish Release Target: - Push to remote(s) after performing finish + Rebase before merging Squash during merge Hotfix: Hotfix Prefix: @@ -422,13 +464,15 @@ Production Branch: Release: Release Prefix: + Start at: Start Feature... - FLOW - Start Feature + Start Feature Start Hotfix... - FLOW - Start Hotfix + Start Hotfix + Name: Enter name Start Release... - FLOW - Start Release + Start Release Version Tag Prefix: Git LFS Add Track Pattern... @@ -459,50 +503,62 @@ Remote: Track files named '{0}' Track all *{0} files + Select Commit HISTORY AUTHOR AUTHOR TIME + COMMIT TIME GRAPH & SUBJECT SHA - COMMIT TIME + HIGHLIGHTS IN GRAPH + All + Current Branch Only + Current Branch & Selected Commits + Selected Commits Only SELECTED {0} COMMITS + SHOW COLUMNS Hold 'Ctrl' or 'Shift' to select multiple commits. Hold ⌘ or ⇧ to select multiple commits. TIPS: + Open in separate window + Commit Detail + Revision Compare Keyboard Shortcuts Reference GLOBAL - Clone new repository + Clone remote repository Close current tab Go to next tab Go to previous tab Create new tab + Open local repository Open Preferences dialog Show workspace dropdown menu Switch active tab + Zoom in/out REPOSITORY Commit staged changes Commit and push staged changes Stage all changes and commit + Create new branch Fetch, starts directly Dashboard mode (Default) + Goto child of selected commit + Goto parent of selected commit Open command palette Commit search mode Pull, starts directly Push, starts directly Force to reload this repository - Switch to 'Changes' - Switch to 'History' - Switch to 'Stashes' - TEXT EDITOR - Close search panel - Find next match - Find previous match - Open with external diff/merge tool - Open search panel + Expand/Collapse details panel in `HISTORY` + Switch to 'LOCAL CHANGES' + Switch to 'HISTORY' + Switch to 'STASHES' Discard Stage Unstage Initialize Repository + Do you want to run `git init` command under this path? + Open repository failed. Reason: Path: Cherry-Pick in progress. Processing commit @@ -514,6 +570,7 @@ Reverting commit Interactive Rebase Stash & reapply local changes + Bypass the pre-rebase hook On: Drag-drop to reorder commits Target Branch: @@ -522,6 +579,7 @@ Commands ERROR NOTICE + New Version Available Open Repositories Tabs Workspaces @@ -530,6 +588,26 @@ Into: Merge Option: Source: + Testing for merge... + Merge can be done without conflicts + Unknown error occurs while testing for merge + Merge will cause conflicts + First Mine, then Theirs + First Theirs, then Mine + USE BOTH + All conflicts resolved + {0} conflict(s) remaining + MINE + Next Conflict + Previous Conflict + RESULT + SAVE & STAGE + THEIRS + Merge Conflicts + Discard unsaved changes? + USE MINE + USE THEIRS + UNDO Merge (Multiple) Commit all changes Strategy: @@ -540,19 +618,24 @@ Move Repository Node Select parent node for: Name: + NO 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 in External Merge Tool + Open Local Repository + Bookmark: + Group: + Folder: Optional. Create New Tab - Bookmark Close Tab Close Other Tabs Close Tabs to the Right Copy Repository Path + Edit Move to Workspace Refresh Repositories @@ -569,14 +652,13 @@ Yesterday Preferences AI - Analyze Diff Prompt + Additional Prompt (Use `-` to list your requirements) API Key - Generate Subject Prompt Model + Fetch available model-ids automatically Name Entered value is the name to load API key from ENV Server - Enable Streaming APPEARANCE Default Font Editor Tab Width @@ -603,26 +685,28 @@ 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 relative time in commit graph Show tags in commit graph Subject Guide Length + 24-Hours + Use compact branch names in commit graph Generate Github style default avatar GIT Enable Auto CRLF Default Clone Dir User Email Global git user email - Enable --prune on fetch - Enable --ignore-cr-at-eol in diff + Prune dead branches after fetching + Ignore CR at end-of-line in text diff Git (>= 2.25.1) is required by this app Install Path Enable HTTP SSL Verify Use git-credential-libsecret instead of git-credential-manager User Name Global git user name + Stash & reapply changes by default when checking-out or merging branches Git version GPG SIGNING Commit GPG signing @@ -646,9 +730,6 @@ Remote Branch: Into: Local Changes: - Discard - Stash & Reapply - Update all submodules Remote: Pull (Fetch & Merge) Use rebase instead of merge @@ -673,7 +754,12 @@ Quit Rebase Current Branch Stash & reapply local changes + Bypass the pre-rebase hook On: + Testing for rebase... + Rebase can be done without conflicts + Unknown error occurs while testing for rebase + Rebase will cause conflicts Add Remote Edit Remote Name: @@ -684,6 +770,7 @@ Custom Action Delete... Edit... + Enable Auto-Fetch Fetch Open In Browser Prune @@ -712,9 +799,12 @@ Open in File Browser Search Branches/Tags/Submodules Visibility in Graph + Collapse Filters Unset Hide in commit graph + Expand Filters Filter in commit graph + filter(s) enabled LAYOUT Horizontal Vertical @@ -726,14 +816,14 @@ Navigate to HEAD Create Branch CLEAR NOTIFICATIONS - Only highlight current branch + Open as Folder Open in {0} Open in External Tools REMOTES Add Remote + RESOLVE Search Commit Author - Committer Content Message Path @@ -751,12 +841,11 @@ Add Submodule Update Submodule TAGS - New Tag + Create Tag By Creator Date By Name Sort Open in Terminal - Use relative time View Logs Visit '{0}' in Browser WORKTREES @@ -774,7 +863,6 @@ Revert Commit Commit: Commit revert changes - Reword Commit Message Running. Please wait... SAVE Save As... @@ -784,9 +872,11 @@ Scan another custom directory Check for Updates... New version of this software is available: + Current Version: Check for updates failed! Download Skip This Version + New Version Release Date: Software Update There are currently no updates available. Set Submodule's Branch @@ -800,9 +890,6 @@ Upstream: Copy SHA Go to - Squash HEAD into Parent - Fixup HEAD into Parent - Into: SSH Private Key: Private SSH key store path START @@ -811,10 +898,12 @@ Message: Optional. Message of this stash Mode: - Only staged changes + Only staged files Both staged and unstaged changes of selected file(s) will be stashed!!! Stash Local Changes Apply + Apply Changes + Checkout New Branch Copy Message Drop Save as Patch... @@ -851,9 +940,15 @@ unmerged Update URL + Submodule Change Details + OPEN DETAILS OK TAGGER TIME + Checkout Tag... + Compare 2 tags + Compare with... + Compare with HEAD Message Name Tagger @@ -866,8 +961,8 @@ Update Submodules All submodules Initialize as needed - Traverse submodules recursively Submodule: + Update nested submodules Update to submodule's remote tracking branch URL: Logs @@ -894,6 +989,7 @@ Ignore *{0} files in the same folder Ignore untracked files in this folder Ignore this file only + Ignore all untracked files in the same folder Amend You can stage this file now. Clear History @@ -907,11 +1003,13 @@ You are creating commit on a detached HEAD. Do you want to continue? You have staged {0} file(s) but only {1} file(s) displayed ({2} files are filtered out). Do you want to continue? CONFLICTS DETECTED - OPEN EXTERNAL MERGETOOL + MERGE + Merge with External Tool OPEN ALL CONFLICTS IN EXTERNAL MERGETOOL FILE CONFLICTS ARE RESOLVED USE MINE USE THEIRS + Filter Changes... INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES @@ -929,9 +1027,13 @@ WORKSPACE: Configure Workspaces... WORKTREE + BRANCH Copy Path + HEAD Lock Open + PATH Remove Unlock + YES diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index acccc2f67..ac2e7d76f 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -5,6 +5,7 @@ Acerca de Acerca de SourceGit + Fecha de Release: {0} Notas de la versión (Release) Cliente Git GUI de código abierto y gratuito Agregar Archivo(s) Para Ignorar @@ -21,15 +22,19 @@ Crear Nueva Rama Rama Existente Asistente OpenAI + Modelo RE-GENERAR Usar OpenAI para generar mensaje de commit - APLICAR COMO MENSAJE DE COMMIT + Usar Ocultar SourceGit + Ocultar Otros Mostrar Todo - Aplicar Parche - Archivo del Parche: + Merge a 3 vías (3-Way) Seleccionar archivo .patch para aplicar Ignorar cambios de espacios en blanco + Fuente: + Archivo + Portapapeles Aplicar Parche Espacios en Blanco: Aplicar Stash @@ -51,16 +56,21 @@ Bisect Abortar Malo - Bisecting. ¿Es el HEAD actual bueno o malo? Bueno Saltar - Bisecting. Marcar el commit actual como bueno o malo y revisar otro. + Bisecting. Por favor revisa otro y márcalo como bueno o malo. + Bisecting. Marca el commit actual como malo o revisa otro y márcalo como malo. + Bisecting. Marcar el commit actual como bueno o malo y revisar otro. + Bisecting. ¿Es el HEAD actual bueno o malo? Blame Blame sobre la Revisión Previa + Ignorar cambios de espacios en blanco ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! Checkout ${0}$... - Comparar con ${0}$ - Comparar con Worktree + Comparar las 2 ramas seleccionadas + Comparar con... + Comparar con HEAD + Comparar con ${0}$ Copiar Nombre de la Rama Crear PR... Crear PR para upstream ${0}$... @@ -71,6 +81,7 @@ Fast-Forward a ${0}$ Fetch ${0}$ en ${1}$... Git Flow - Finalizar ${0}$ + Hacer Rebase interactivamente ${0}$ en ${1}$ Merge ${0}$ en ${1}$... Hacer merge de las ramas {0} seleccionadas hacia la rama actual Pull ${0}$ @@ -81,7 +92,6 @@ Resetear ${0}$ a ${1}$... Cambiar a ${0}$ (worktree) Establecer Rama de Seguimiento... - Comparar Ramas {0} commit(s) adelante {0} commit(s) adelante, {1} commit(s) detrás {0} commit(s) detrás @@ -95,6 +105,9 @@ Resetear a Revisión Padre Resetear a Esta Revisión Generar mensaje de commit + Merge (Incorporado) + Merge (Externa) + Resetear Archivo(s) a ${0}$ CAMBIAR MODO DE VISUALIZACIÓN Mostrar como Lista de Archivos y Directorios Mostrar como Lista de Rutas @@ -103,17 +116,18 @@ Submódulo: URL: Checkout Rama - Checkout Commit - Commit: - Advertencia: Al hacer un checkout de commit, tu Head se separará Cambios Locales: - Descartar - Stash & Reaplicar - Actualizar todos los submódulos Rama: ¡Tu HEAD actual contiene commit(s) que no están conectados a ningunas ramas/etiquetas! ¿Quieres continuar? - Checkout & Fast-Forward + Los siguientes submódulos necesitan ser actualizados:{0} ¿Quieres actualizarlos? + Checkout y Fast-Forward Fast-Forward a: + Checkout a la Rama desde el Stash + Nueva Rama: + Stash: + Hacer checkout a Commit o Etiqueta + Objetivo: + Advertencia: Después de hacer esto, se deprenderá/separará de HEAD Cherry Pick Añadir fuente al mensaje de commit Commit(s): @@ -125,6 +139,8 @@ Clonar Repositorio Remoto Parámetros Adicionales: Argumentos adicionales para clonar el repositorio. Opcional. + Marcador: + Grupo: Nombre Local: Nombre del repositorio. Opcional. Carpeta Padre: @@ -132,18 +148,23 @@ URL del Repositorio: CERRAR Editor + Ramas + Ramas y Etiquetas + Acciones Personalizadas del Repositorio + Archivos de Revisión Checkout Commit Cherry-Pick Este Commit Cherry-Pick ... Comparar con HEAD Comparar con Worktree Autor + Fecha/Hora del Autor Mensaje Committer + Fecha/Hora del Committer SHA Asunto Acción personalizada - Eliminar Commit Rebase interactivo Eliminar... Editar... @@ -151,32 +172,28 @@ Hacer Rebase interactivamente ${0}$ en ${1}$ Reescribir... Squash en el Padre... - Merge a ${0}$ Merge ... Push ${0}$ a ${1}$ Rebase ${0}$ en ${1}$ Resetear ${0}$ a ${1}$ Revertir Commit - Reescribir Guardar como Parche... - Squash en el Padre - Arreglar en el Padre CAMBIOS archivo(s) modificado(s) Buscar Cambios... + Colapsar detalles ARCHIVOS Archivo LFS Buscar Archivos... Submódulo INFORMACIÓN AUTOR - HIJOS COMMITTER Ver refs que contienen este commit COMMIT ESTÁ CONTENIDO EN Copiar Email Copiar Nombre - Copiar Nombre & Email + Copiar Nombre y Email Muestra solo los primeros 100 cambios. Ver todos los cambios en la pestaña CAMBIOS. Clave: MENSAJE @@ -185,12 +202,19 @@ SHA Firmante: Abrir en Navegador + COL Ingresa el mensaje de commit. ¡Por favor usa una línea en blanco para separar el título y la descripción! ASUNTO + Comparar + CAMBIOS + COMMITS + SOLO COMMITS A LA IZQUIERDA + SOLO COMMITS A LA DERECHA + Tips: Puedes hacer cherry-pick a los commits seleccionados si el otro lado está en HEAD (usando el menú contextual). Configurar Repositorio PLANTILLA DE COMMIT - Parámetros incorporados: - + Parámetros incorporados: + ${branch_name} Nombre de la rama local actual. ${files_num} Número de archivos modificados ${files} Rutas de archivos modificados @@ -201,8 +225,8 @@ Nombre de la Plantilla: ACCIÓN PERSONALIZADA Argumentos: - Parámetros incorporados: - + 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 @@ -226,10 +250,12 @@ Dirección de Email Dirección de email GIT + Preguntar antes de actualizar automáticamente los submódulos Fetch remotos automáticamente Minuto(s) Tipos de Commit Convencionales Remoto por Defecto + Habilitar `--recursive` cuando se actualicen automáticamente los submódulos Modo preferido de Merge SEGUIMIENTO DE INCIDENCIAS Añadir Regla de Ejemplo para Azure DevOps @@ -262,15 +288,19 @@ Etiqueta: Opciones: Usar '|' como delimitador para las opciones + Formateador de Cadenas de texto: + Opcional. Utilizado para formatear la cadena de salida. Ignorado cuando la entrada está vacía. Por favor utiliza `${VALUE}` para representar la cadena de entrada. La variables incorporadas ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, y ${TAG} permanecen disponibles aquí Tipo: + Utilizar Nombre Amigable: Espacios de Trabajo Color Nombre Restaurar pestañas al iniciar CONTINUAR ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty)? - HACER STAGE A TODO & COMMIT + HACER STAGE A TODO y COMMIT + STAGE LO SELECCIONADO y COMMIT ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty) o hacer stage a todo y después commit? Reinicio Requerido Necesita reiniciar esta aplicación para aplicar los cambios. @@ -283,14 +313,13 @@ Tipo de Cambio: Copiar Copiar Todo el Texto + Copiar como Parche Copiar Ruta Completa Copiar Ruta Crear Rama... Basado En: Checkout de la rama creada Cambios Locales: - Descartar - Stash & Reaplicar Nombre de la Nueva Rama: Introduzca el nombre de la rama. Crear Rama Local @@ -309,13 +338,17 @@ ligera Mantenga Ctrl para iniciar directamente Cortar + Descartar + No Hacer Nada + Stash y Volver a aplicar Desinicializar Submódulo Forzar desinicialización incluso si contiene cambios locales. Submódulo: Eliminar Rama + ¿Quieres que también se elimine la siguiente rama remota? Rama: + Forzar la eliminación incluso si contiene commit(s) sin fusionar ¡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 comprobar dos veces antes de realizar esta acción! Eliminar Múltiples Etiquetas @@ -335,7 +368,7 @@ Etiqueta: Eliminar de los repositorios remotos DIFERENCIA BINARIA - Modo de Archivo Cambiado + ARCHIVO VACÍO Primera Diferencia Ignorar Cambio de Espacios en Blanco MEZCLAR @@ -355,6 +388,7 @@ SUBMÓDULO BORRADO NUEVO + + {0} Cambios Sin confirmar Intercambiar Resaltado de Sintaxis Ajuste de Línea @@ -371,12 +405,10 @@ Todos los cambios locales en la copia de trabajo. Cambios: Incluir archivos ignorados - Incluir archivos no rastreados + Incluir archivos modificados/eliminados + Incluir archivos sin rastrear 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: @@ -409,15 +441,25 @@ Historial de Archivos CAMBIO CONTENIDO + Cambiar modo archivo: + Modo archivo eliminado: + Directorio + Ejecutable + Modo archivo nuevo: + Normal + Submódulo + Enlace simbólico (Symlink) + Desconocido Git-Flow Rama de Desarrollo: Feature: Prefijo de Feature: + Finalizar ${0}$ FLOW - Finalizar Feature FLOW - Finalizar Hotfix FLOW - Finalizar Release Destino: - Push al/los remoto(s) después de Finalizar + Hacer rebase antes de fusionar Squash durante el merge Hotfix: Prefijo de Hotfix: @@ -426,10 +468,12 @@ Rama de Producción: Release: Prefijo de Release: + Iniciar en: Iniciar Feature... FLOW - Iniciar Feature Iniciar Hotfix... FLOW - Iniciar Hotfix + Nombre: Introducir nombre Iniciar Release... FLOW - Iniciar Release @@ -463,16 +507,26 @@ Remoto: Seguir archivos llamados '{0}' Seguir todos los archivos *{0} + Seleccionar Commit Historias AUTOR HORA DEL AUTOR - GRÁFICO & ASUNTO + FECHA DE COMMIT + GRÁFICO y ASUNTO SHA - FECHA DE COMMIT + DESTACADOS EN EL GRÁFICO + Todos + Solo la Rama Actual + Rama Actual y los Commits Seleccionados + Solo los Commits Seleccionados {0} COMMITS SELECCIONADOS + MOSTRAR COLUMNAS Mantén 'Ctrl' o 'Shift' para seleccionar múltiples commits. Mantén ⌘ o ⇧ para seleccionar múltiples commits. CONSEJOS: + Abrir en una ventana separada + Detalles del Commit + Comparación de Revisiones Referencia de Atajos de Teclado GLOBAL Clonar repositorio nuevo @@ -480,32 +534,35 @@ Ir a la siguiente página Ir a la página anterior Crear nueva página + Abrir repositorio local Abrir diálogo de preferencias + Mostrar menú desplegable del espacio de trabajo Cambiar página activa + Acercar/Alejar REPOSITORIO Commit cambios staged Commit y push cambios staged Stage todos los cambios y commit + Crear nueva rama Fetch, empieza directamente Modo Dashboard (Por Defecto) + Ir al hijo del commit seleccionado + Ir al padre del commit seleccionado Abrir paleta de comandos Modo de búsqueda de commits Pull, empieza directamente Push, empieza directamente Forzar a recargar este repositorio + Expandir/Colapsar el panel de detalles en el `HISTORIAL`/`HISTORY` Cambiar a 'Cambios' Cambiar a 'Historias' Cambiar a 'Stashes' - EDITOR DE TEXTO - Cerrar panel de búsqueda - Buscar siguiente coincidencia - Buscar coincidencia anterior - Abrir con herramienta diff/merge externa - Abrir panel de búsqueda Descartar Stage Unstage Inicializar Repositorio + ¿Quieres correr el comando `git init` en esta ruta? + Falló la apertura del repositorio. Razón: Ruta: Cherry-Pick en progreso. Procesando commit @@ -516,7 +573,8 @@ Revert en progreso. Haciendo revert del commit Rebase Interactivo - Stash & reaplicar cambios locales + Stash y volver a aplicar cambios locales + Saltar el hook de pre-rebase En: Arrastrar y soltar para reordenar commits Rama Objetivo: @@ -525,6 +583,7 @@ Comandos ERROR AVISO + Nueva Versión Disponible Abrir Repositorios Páginas Espacios de trabajo @@ -533,7 +592,27 @@ En: Opción de Merge: Rama Fuente: - Merge (Multiplo) + Pruebas para hacer merge... + El merge se puede realizar sin conflictos + Error desconocido ocurrido al probar el merge + Hacer merge causará conflictos + Primero Míos, después Suyos + Primero Suyos, después Míos + USAR AMBOS + Todos los conflictos resueltos + {0} conflicto(s) restantes + MÍOS + Conflicto Siguiente + Conflicto Previo + RESULTADO + GUARDAR y hacer STAGE + SUYOS + Conflictos de Merge + ¿Descartar cambios sin guardar? + USAR MÍOS + USAR SUYOS + DESHACER + Merge (Multiple) Commit todos los cambios Estrategia: Destino: @@ -543,19 +622,26 @@ Mover Nodo del Repositorio Seleccionar nodo padre para: Nombre: + NO 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 Repositorio Local + Marcador: + Grupo: + Carpeta: Opcional. Crear Nueva Página - Marcador Cerrar Pestaña Cerrar Otras Pestañas Cerrar Pestañas a la Derecha Copiar Ruta del Repositorio + Editar + Mover al Espacio de trabajo + Actualizar Repositorios Pegar Hace {0} días @@ -570,14 +656,13 @@ Ayer Preferencias OPEN AI - Analizar Diff Prompt + Prompt adicional (Usa `-` para listar tus requerimientos) Clave API - Generar Subject Prompt Modelo + Traer automáticamente los model-ids disponibles Nombre El valor ingresado es el nombre de la clave API a cargar desde ENV Servidor - Habilitar Transmisión APARIENCIA Fuente por defecto Ancho de la Pestaña del Editor @@ -591,6 +676,10 @@ 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 @@ -600,12 +689,13 @@ 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 hora relativa en el gráfico de commits Mostrar etiquetas en el gráfico de commit Longitud de la guía del asunto + 24-Horas + Utilizar nombres de ramas compactos en el gráfico de commits Generar avatar con estilo por defecto de Github GIT Habilitar Auto CRLF @@ -620,6 +710,7 @@ Usar git-credential-libsecret en lugar de git-credential-manager Nombre del usuario Nombre global del usuario git + Hacer Stash y volver a aplicar cambios por defecto cuando se haga checkout o fusionen las ramas Versión de Git FIRMA GPG Firma GPG en commit @@ -631,6 +722,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 @@ -641,9 +734,6 @@ Rama Remota: En: Cambios Locales: - Descartar - Stash & Reaplicar - Actualizar todos los submódulos Remoto: Pull (Fetch & Merge) Usar rebase en lugar de merge @@ -667,8 +757,13 @@ Nombre de entrada de la nueva rama remota: Salir Rebase Rama Actual - Stash & reaplicar cambios locales + Stash y volver a aplicar cambios locales + Saltar el hook de pre-rebase En: + Pruebas para hacer rebase ... + El rebase se puede realizar sin conflictos + Error desconocido ocurrido al probar el rebase + Hacer rebase causará conflictos Añadir Remoto Editar Remoto Nombre: @@ -679,6 +774,7 @@ Acción Personalizada Borrar... Editar... + Habilitar Auto-Fetch Fetch Abrir En Navegador Podar (Prune) @@ -707,9 +803,12 @@ Abrir en el Explorador Buscar Ramas/Etiquetas/Submódulos Visibilidad en el Gráfico + Colapsar Filtros Desestablecer Ocultar en el Gráfico de Commits + Expandir Filtros Filtrar en el Gráfico de Commits + filtro(s) activado(s) DISPOSICIÓN Horizontal Vertical @@ -721,14 +820,14 @@ Navegar a HEAD Crear Rama LIMPIAR NOTIFICACIONES - Sólo resaltar la rama actual + Abrir como Carpeta Abrir en {0} Abrir en Herramientas Externas REMOTOS AÑADIR REMOTO + RESOLVER Buscar Commit Autor - Committer Contenido Mensaje Ruta @@ -751,7 +850,6 @@ Por Nombre Ordenar Abrir en Terminal - Usar tiempo relativo Ver Logs Visitar '{0}' en el Navegador WORKTREES @@ -769,7 +867,6 @@ Revertir Commit Commit: Commit revertir cambios - Reescribir Mensaje de Commit Ejecutando. Por favor espera... GUARDAR Guardar Como... @@ -779,9 +876,11 @@ Escanear otro directorio personalizado Buscar Actualizaciones... Nueva versión de este software disponible: + Versión Actual: ¡Error al buscar actualizaciones! Descargar Omitir Esta Versión + Fecha de Release de la Nueva Versión: Actualización de Software Actualmente no hay actualizaciones disponibles. Establecer Rama del Submódulo @@ -799,7 +898,7 @@ Ruta de almacenamiento de la clave privada SSH INICIAR Stash - Incluir archivos no rastreados + Incluir archivos sin rastrear Mensaje: Opcional. Información de este stash Modo: @@ -807,6 +906,8 @@ ¡Tanto los cambios staged como los no staged de los archivos seleccionados serán stashed! Stash Cambios Locales Aplicar + Aplicar Cambios + Checkout a Nueva Rama Copiar Mensaje Eliminar Guardar como Parche... @@ -843,9 +944,15 @@ unmerged Actualizar URL + Cambiar Detalles del Submódulo + ABRIR DETALLES OK ETIQUETADOR HORA + Hacer checkout a Etiqueta... + Comparar 2 etiquetas + Comparar con... + Comparar con HEAD Mensaje Nombre Etiquetador @@ -853,13 +960,13 @@ Acción Personalizada Eliminar ${0}$... Eliminar {0} etiquetas seleccionadas... - Merge ${0}$ en ${1}$... - Push ${0}$... + Hacer merge de ${0}$ a ${1}$... + Hacer push a ${0}$... Actualizar Submódulos Todos los submódulos Inicializar según sea necesario - Recursivamente Submódulo: + Actualizar submódulos anidados Actualizar la rama de seguimiento remota del submódulo URL: Logs @@ -884,14 +991,15 @@ Git Ignore Ignorar todos los archivos *{0} Ignorar archivos *{0} en la misma carpeta - Ignorar archivos no rastreados en esta carpeta + Ignorar archivos sin rastrear en esta carpeta Ignorar solo este archivo + Ignorar todos los archivos sin rastrear en la misma carpeta 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 + COMMIT y PUSH Plantilla/Historias Activar evento de clic Commit (Editar) @@ -899,12 +1007,13 @@ Estas creando un commit en una HEAD separada. ¿Quieres continuar? Tienes {0} archivo(s) en stage, pero solo {1} archivo(s) mostrado(s) ({2} archivo(s) están filtrados). ¿Quieres continuar? CONFLICTOS DETECTADOS - ABRIR HERRAMIENTA DE MERGE EXTERNA + MERGE + Merge con Herramienta Externa ABRIR TODOS LOS CONFLICTOS EN HERRAMIENTA DE MERGE EXTERNA LOS CONFLICTOS DE ARCHIVOS ESTÁN RESUELTOS USAR MÍOS USAR SUYOS - INCLUIR ARCHIVOS NO RASTREADOS + INCLUIR ARCHIVOS SIN RASTREAR NO HAY MENSAJES DE ENTRADA RECIENTES NO HAY PLANTILLAS DE COMMIT Sin verificar @@ -921,9 +1030,13 @@ ESPACIO DE TRABAJO: Configura Espacios de Trabajo... WORKTREE + RAMA Copiar Ruta + HEAD Bloquear Abrir + RUTA Eliminar Desbloquear + diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 2178b207e..a094fb2ae 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -5,6 +5,7 @@ À propos À propos de SourceGit + Date de publication : {0} Notes de Version Client Git Open Source et Gratuit Ajouter le(s) Fichier(s) à Ignorer @@ -21,13 +22,14 @@ Créer une nouvelle branche Branche existante Assistant IA + Modèle RE-GÉNÉRER Utiliser l'IA pour générer un message de commit - APPLIQUER COMME MESSAGE DE COMMIT + Utiliser Masquer SourceGit + Masquer les autres Tout Afficher - Appliquer - Fichier de patch : + Fusion à 3 voies Selectionner le fichier .patch à appliquer Ignorer les changements d'espaces blancs Appliquer le patch @@ -51,25 +53,30 @@ 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. + Bisect en cours. Marquer le commit actuel comme bon ou mauvais et en récupérer un autre. + Bisect en cours. Le HEAD actuel est-il bon ou mauvais ? Blâme Blâmer sur la révision précédente + Ignorer les changements d'espaces LE BLÂME SUR CE FICHIER N'EST PAS SUPPORTÉ!!! Récupérer ${0}$... - Comparer avec ${0}$ - Comparer avec le worktree + Comparer les 2 branches sélectionnées + Comparer avec... + Comparer avec HEAD + Comparer avec ${0}$ 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 + Modifier la description de ${0}$... Fast-Forward vers ${0}$ Fetch ${0}$ vers ${1}$... Git Flow - Terminer ${0}$ + Rebaser Interactivement ${0}$ sur ${1}$ Fusionner ${0}$ dans ${1}$... Fusionner les {0} branches sélectionnées dans celle en cours Tirer ${0}$ @@ -80,7 +87,6 @@ Réinitialiser ${0}$ sur ${1}$... Basculer vers ${0}$ (worktree) Définir la branche de suivi... - Comparer les branches {0} commit(s) en avance {0} commit(s) en avance, {1} commit(s) en retard {0} commit(s) en retard @@ -94,6 +100,9 @@ Réinitialiser à la révision parente Réinitialiser à cette révision Générer un message de commit + Fusion (intégré) + Fusion (externe) + Réinitialiser le(s) fichier(s) à ${0}$ CHANGER LE MODE D'AFFICHAGE Afficher comme liste de dossiers/fichiers Afficher comme liste de chemins @@ -102,17 +111,15 @@ 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 à jour tous les sous-modules Branche : Votre HEAD actuel contient un ou plusieurs commits non connectés à une branche/tag ! Voulez-vous continuer ? + Les sous-modules suivants doivent être mis à jour :{0}Voulez-vous les mettre à jour ? Récupérer & Fast-Forward Fast-Forward vers : + Créer une branche depuis le stash + Nouvelle branche : + Stash : Cherry-Pick de ce commit Ajouter la source au message de commit Commit : @@ -124,6 +131,8 @@ Cloner repository distant Paramètres supplémentaires : Arguments additionnels au clônage. Optionnel. + Signet : + Groupe : Nom local : Nom de dépôt. Optionnel. Dossier parent : @@ -131,6 +140,10 @@ URL du dépôt : FERMER Éditeur + Branches + Branches et tags + Actions personnalisées du dépôt + Fichiers de révision Récupérer ce commit Cherry-Pick ce commit Cherry-Pick ... @@ -142,7 +155,6 @@ SHA Sujet Action personnalisée - Supprimer le Commit Rebase Interactif Supprimer... Modifier... @@ -150,26 +162,22 @@ 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 - Fixup dans le Parent CHANGEMENTS fichier(s) modifié(s) Rechercher les changements... + Réduire les détails FICHIERS Fichier LFS Rechercher des fichiers... Sous-module INFORMATIONS AUTEUR - ENFANTS COMMITTER Vérifier les références contenant ce commit LE COMMIT EST CONTENU PAR @@ -184,11 +192,19 @@ SHA Signataire : Ouvrir dans le navigateur + COL + Saisissez le message de commit. Utilisez une ligne vide pour séparer le sujet et la description ! SUJET + Comparer + CHANGEMENTS + COMMITS + COMMITS UNIQUEMENT À GAUCHE + COMMITS UNIQUEMENT À DROITE + Astuce : vous pouvez cherry-pick les commits sélectionnés si l'autre côté est HEAD (via le menu contextuel). Configurer le dépôt MODÈLE DE COMMIT - Paramètres intégrés : - + 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 @@ -199,8 +215,8 @@ Nom de modèle: ACTION PERSONNALISÉE Arguments : - Paramètres intégrés : - + 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 @@ -224,6 +240,7 @@ Adresse e-mail Adresse e-mail GIT + Demander avant mise à jour automatique des sous-modules Fetch les dépôts distants automatiquement minute(s) Types de commit conventionnels @@ -260,8 +277,11 @@ Libellé : Options : Utiliser '|' comme délimiteur pour les options + Formateur de chaîne : + Optionnel. Utilisé pour formater la sortie. Ignoré si vide. Utilisez `${VALUE}` pour représenter la chaîne d'entrée. Les variables intégrées ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, et ${TAG} restent disponibles ici Type : + Utiliser le nom convivial : Espaces de travail Couleur Nom @@ -269,6 +289,7 @@ CONTINUER Commit vide détecté ! Voulez-vous continuer (--allow-empty) ? TOUT INDEXER & COMMIT + INDEXER LA SÉLECTION & 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. @@ -287,8 +308,6 @@ Basé sur : Récupérer la branche créée Changements locaux : - Rejeter - Stash & Réappliquer Nom de la nouvelle branche : Entrez le nom de la branche. Créer une branche locale @@ -307,13 +326,15 @@ léger Maintenir Ctrl pour commencer directement Couper + Annuler + Ne rien faire + Mettre en stash et réappliquer 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 @@ -333,7 +354,6 @@ Tag : Supprimer des dépôts distants DIFF BINAIRE - Mode de fichier changé Première différence Ignorer les changements d'espaces FUSIONNER @@ -353,6 +373,7 @@ SOUS-MODULE SUPPRIMÉ NOUVEAU + + {0} changements non commités Permuter Coloration syntaxique Retour à la ligne @@ -369,12 +390,12 @@ Tous les changements dans la copie de travail. Changements : Inclure les fichiers ignorés + Inclure les fichiers modifiés/supprimés Inclure les fichiers non suivis {0} changements seront rejetés Vous ne pouvez pas annuler cette action !!! - Supprimer le Commit - Commit : - Nouveau HEAD : + Modifier la description de la branche + Cible : Signet : Nouveau nom : Cible : @@ -389,6 +410,7 @@ Remote : Récupérer les changements distants Présumer inchangé + Action personnalisée Rejeter... Rejeter {0} fichiers... Résoudre en utilisant ${0}$ @@ -412,7 +434,6 @@ 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: @@ -458,16 +479,26 @@ Dépôt : Suivre les fichiers appelés '{0}' Suivre tous les fichiers *{0} + Sélectionner un commit Historique AUTEUR HEURE DE L'AUTEUR + HEURE DE COMMIT GRAPHE & SUJET SHA - HEURE DE COMMIT + MISE EN ÉVIDENCE AU GRAPHE + Tout + Branche courante uniquement + Branche courante et commits sélectionnés + Commits sélectionnés uniquement {0} COMMITS SÉLECTIONNÉS + AFFICHER LES COLONNES Maintenir 'Ctrl' ou 'Shift' enfoncée pour sélectionner plusieurs commits. Maintenir ⌘ ou ⇧ enfoncée pour sélectionner plusieurs commits. CONSEILS: + Ouvrir dans une fenêtre séparée + Détails du commit + Comparaison de révision Référence des raccourcis clavier GLOBAL Cloner un nouveau dépôt @@ -475,32 +506,35 @@ Aller à la page suivante Aller à la page précédente Créer une nouvelle page + Ouvrir un dépôt local Ouvrir le dialogue des préférences + Afficher le menu des espaces de travail Changer d'onglet actif + Zoom avant/arrière DÉPÔT Commit les changements de l'index Commit et pousser les changements de l'index Ajouter tous les changements et commit + Créer une nouvelle branche Fetch, démarre directement Mode tableau de bord (Défaut) + Aller au commit enfant + Aller au commit parent Ouvrir la palette de commandes Recherche de commit Pull, démarre directement Push, démarre directement Forcer le rechargement du dépôt + Afficher/Masquer le panneau de détails du panneau`HISTORIQUE` Basculer vers 'Changements' Basculer vers 'Historique' Basculer vers 'Stashes' - ÉDITEUR DE TEXTE - 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 Retirer de l'index Initialiser le repository + Voulez-vous exécuter `git init` dans ce dossier ? + Échec de l'ouverture du dépôt. Raison : Chemin : Cherry-Pick en cours. Traitement du commit @@ -512,6 +546,7 @@ Annulation du commit Rebase interactif Stash & réappliquer changements locaux + Contourner le hook de pre-rebase Sur : Glisser-déposer pour réorganiser les commits Branche cible : @@ -528,6 +563,22 @@ Dans : Option de merge: Source: + D'abord les miens, puis les leurs + D'abord les leurs, puis les miens + UTILISER LES DEUX + Tous les conflits sont résolus + {0} conflit(s) restant(s) + MIENS + Conflit suivant + Conflit précédent + RÉSULTAT + SAUVER ET INDEXER + LEURS + Conflits de fusion + Abandonner les modifications non sauvegardées ? + UTILISER LES MIENS + UTILISER LES LEURS + ANNULER Fusionner (Plusieurs) Commit tous les changement Stratégie: @@ -538,18 +589,26 @@ Déplacer le noeud du repository Sélectionnier le noeud parent pour : Nom : + NON 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 un fichier Ouvrir dans l'outil de fusion + Ouvrir un dépôt local + Signet : + Groupe : + Dossier : Optionnel. Créer un nouvel onglet - Bookmark Fermer l'onglet Fermer les autres onglets Fermer les onglets à droite Copier le chemin vers le dépôt + Éditer + Se déplacer vers l'espace de travail + Actualiser Dépôts Coller il y a {0} jours @@ -564,14 +623,13 @@ Hier Préférences IA - Analyser Diff Prompt + Prompt supplémentaire (utilisez `-` pour lister vos exigences) Clé d'API - Générer le sujet de Prompt Modèle + Récupérer automatiquement les modèles disponibles Nom La valeur saisie est le nom pour charger la clé API depuis l'ENV Serveur - Activer le streaming APPARENCE Police par défaut Largeur de tab dans l'éditeur @@ -585,6 +643,10 @@ Utiliser des onglets de taille fixe dans la barre de titre Utiliser un cadre de fenêtre natif OUTIL DIFF/MERGE + Arguments de diff + Variables disponibles : $LOCAL, $REMOTE + Arguments de merge + Variables disponibles : $BASE, $LOCAL, $REMOTE, $MERGED Chemin d'installation Saisir le chemin d'installation de l'outil diff/merge Outil @@ -594,12 +656,12 @@ 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 le temps relatif dans le graphe de commits Afficher les tags dans le graphique des commits Guide de longueur du sujet + Format 24 heures Générer un avatar par défaut de style GitHub GIT Activer auto CRLF @@ -614,6 +676,7 @@ Utiliser git-credential-libsecret au lieu de git-credential-manager Nom d'utilisateur Nom d'utilisateur global + Utiliser stash & réappliquer par défaut lors des checkout/merge Version de Git SIGNATURE GPG Signature GPG de commit @@ -625,6 +688,8 @@ Clé de signature GPG de l'utilisateur INTEGRATION SHELL/TERMINAL + Arguments + Utilisez '.' pour représenter le répertoire de travail Chemin Shell/Terminal Élaguer une branche distant @@ -635,9 +700,6 @@ Branche distante : Dans : Changements locaux : - Rejeter - Stash & Réappliquer - Mettre à jour tous les sous-modules Dépôt distant : Pull (Fetch & Merge) Utiliser rebase au lieu de merge @@ -662,6 +724,7 @@ Quitter Rebase la branche actuelle Stash & réappliquer changements locaux + Contourner le hook de pre-rebase Sur : Ajouter dépôt distant Modifier dépôt distant @@ -673,6 +736,7 @@ Action personnalisée Supprimer... Editer... + Activer l'auto-fetch Fetch Ouvrir dans le navigateur Elaguer @@ -715,14 +779,14 @@ Naviguer vers le HEAD Créer une branche EFFACER LES NOTIFICATIONS - Surligner uniquement la branche actuelle + Ouvrir comme dossier Ouvrir dans {0} Ouvrir dans un outil externe DEPOTS DISTANTS AJOUTER DEPOT DISTANT + RÉSOUDRE Rechercher un commit Auteur - Committer Contenu Message Chemin @@ -745,7 +809,6 @@ Par nom Trier Ouvrir dans un terminal - Utiliser l'heure relative Voir les Logs Visiter '{0}' dans le navigateur WORKTREES @@ -763,7 +826,6 @@ Annuler le Commit Commit : Commit les changements de l'annulation - Reformuler le message de commit En exécution. Veuillez patienter... SAUVEGARDER Sauvegarder en tant que... @@ -773,9 +835,11 @@ Scanner un autre répertoire personnalisé Rechercher des mises à jour... Une nouvelle version du logiciel est disponible : + Version actuelle : La vérification de mise à jour à échouée ! Télécharger Passer cette version + Date de la nouvelle version : Mise à jour du logiciel Il n'y a pas de mise à jour pour le moment. Définir la Branche du Sous-module @@ -801,6 +865,8 @@ Les modifications indexées et non-indexées des fichiers sélectionnés seront stockées!!! Stash les changements locaux Appliquer + Appliquer les changements + Créer une branche Copier le Message Effacer Sauver comme Patch... @@ -837,9 +903,14 @@ non fusionné Mettre à jour URL + Détails des changements du sous-module + OUVRIR LES DÉTAILS OK TAGUEUR HEURE + Comparer 2 tags + Comparer avec... + Comparer avec HEAD Message Nom Tagueur @@ -847,12 +918,10 @@ Action personnalisée Supprimer ${0}$... Supprimer les {0} tags sélectionnés... - Fusionner ${0}$ dans ${1}$... Pousser ${0}$... Actualiser les sous-modules Tous les sous-modules Initialiser au besoin - Récursivement Sous-module : Mettre à jour vers la branche de suivi distante du sous-module URL : @@ -893,7 +962,8 @@ 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 + FUSIONNER + Fusion avec un outil externe OUVRIR TOUS LES CONFLITS DANS L'OUTIL DE FUSION EXTERNE LES CONFLITS DE FICHIER SONT RÉSOLUS UTILISER LES MIENS @@ -915,9 +985,13 @@ ESPACE DE TRAVAIL : Configurer les espaces de travail... WORKTREE + BRANCHE Copier le chemin + HEAD Verrouiller Ouvrir + CHEMIN Supprimer Déverrouiller + OUI diff --git a/src/Resources/Locales/he_IL.axaml b/src/Resources/Locales/he_IL.axaml new file mode 100644 index 000000000..64346545d --- /dev/null +++ b/src/Resources/Locales/he_IL.axaml @@ -0,0 +1,997 @@ + + + + + + אודות + אודות SourceGit + תאריך שחרור: {0} + הערות שחרור + לקוח Git גרפי, חופשי ובקוד פתוח + הוסף קבצים ל־Ignore + תבנית: + קובץ אחסון: + הוסף Worktree + מיקום: + נתיב ל־worktree זה. נתיב יחסי נתמך. + שם Branch: + לא חובה. ברירת המחדל היא שם תיקיית היעד. + Branch למעקב: + מעקב אחר branch מרוחק + מה לבצע לו checkout: + צור Branch חדש + Branch קיים + עוזר AI + מודל + צור מחדש + שימוש ב־AI ליצירת הודעת commit + שימוש + הסתר SourceGit + הסתר אחרים + הצג הכול + 3-Way Merge + בחר קובץ .patch להחלה + התעלמות משינויי whitespace + החל Patch + Whitespace: + החל Stash + מחק לאחר ההחלה + שחזר שינויי ה־index + Stash: + ארכב... + שמור ארכיון אל: + בחר נתיב לקובץ הארכיון + Revision: + ארכב + SourceGit Askpass + הזן passphrase: + קבצים בהנחת assume-unchanged + אין קבצים בהנחת assume-unchanged + טען תמונה... + רענן + קובץ בינארי אינו נתמך!!! + Bisect + ביטול + פגום (Bad) + תקין (Good) + דלג + מתבצע Bisect. יש לסמן את ה־commit הנוכחי כתקין או כפגום ולבצע checkout לאחר. + מתבצע Bisect. האם ה־HEAD הנוכחי תקין או פגום? + Blame + Blame ל־revision הקודם + התעלמות משינויי whitespace + לא ניתן לבצע Blame על קובץ מסוג זה!!! + Checkout ל־${0}$... + השווה בין 2 ה־branches שנבחרו + השווה עם... + השווה עם HEAD + השווה עם ${0}$ + העתק שם ה־Branch + צור PR... + צור PR ל־upstream ${0}$... + פעולה מותאמת אישית + מחק ${0}$... + מחק {0} ה־branches שנבחרו + ערוך תיאור ל־${0}$... + Fast-Forward אל ${0}$ + Fetch של ${0}$ אל ${1}$... + Git Flow - סיים ${0}$ + Interactive Rebase של ${0}$ על ${1}$ + Merge של ${0}$ אל ${1}$... + Merge של {0} ה־branches שנבחרו אל הנוכחי + Pull ${0}$ + Pull של ${0}$ אל ${1}$... + Push ${0}$ + Rebase של ${0}$ על ${1}$... + שנה שם ${0}$... + Reset של ${0}$ אל ${1}$... + עבור אל ${0}$ (worktree) + הגדר Tracking Branch... + {0} commits לפני + {0} commits לפני, {1} commits אחרי + {0} commits אחרי + לא תקין + REMOTE + סטטוס + TRACKING + URL + WORKTREE + ביטול + Checkout ל־revision של ההורה + Checkout ל־revision זה + צור הודעת commit + Merge (מובנה) + Merge (כלי חיצוני) + Reset של הקבצים אל ${0}$ + שנה מצב תצוגה + הצג כרשימת קבצים ותיקיות + הצג כרשימת נתיבים + הצג כעץ מערכת קבצים + שנה ה־URL של Submodule + Submodule: + URL: + Checkout של Branch + שינויים מקומיים: + Branch: + ה־HEAD הנוכחי מכיל commits שאינם מקושרים לאף branch או tag! האם להמשיך? + יש לעדכן את ה־submodules הבאים:{0}לעדכן אותם? + Checkout ו־Fast-Forward + Fast-Forward אל: + Checkout של Branch מתוך Stash + Branch חדש: + Stash: + Cherry-Pick + הוסף המקור להודעת ה־commit + Commits: + צור commit לכל השינויים + Mainline: + בדרך כלל לא ניתן לבצע cherry-pick ל־merge מכיוון שאין דרך לדעת איזה צד של ה־merge צריך להיחשב כ־mainline. אפשרות זו מאפשרת ל־cherry-pick לשחזר את השינוי ביחס להורה שצוין. + נקה Stashes + פעולה זו תנקה את כל ה־stashes. להמשיך? + שכפל מאגר מרוחק + פרמטרים נוספים: + ארגומנטים נוספים ל־clone. לא חובה. + סימנייה: + קבוצה: + שם מקומי: + שם המאגר. לא חובה. + תיקיית אב: + אתחול ועדכון submodules + URL של המאגר: + סגור + עורך + Branches + Branches ו־Tags + פעולות מותאמות של המאגר + קבצי Revision + Checkout של Commit + Cherry-Pick של Commit + Cherry-Pick ... + השווה עם HEAD + השווה עם ה־Worktree + Author + הודעה + Committer + SHA + Subject + פעולה מותאמת אישית + Interactive Rebase + Drop... + Edit... + Fixup לתוך ההורה... + Interactive Rebase של ${0}$ על ${1}$ + Reword... + Squash לתוך ההורה... + Merge ... + Push של ${0}$ אל ${1}$ + Rebase של ${0}$ על ${1}$ + Reset של ${0}$ אל ${1}$ + Revert של Commit + שמור כ־Patch... + שינויים + קבצים שונו + חיפוש בשינויים... + כיווץ הפרטים + קבצים + קובץ LFS + חיפוש קבצים... + Submodule + מידע + AUTHOR + COMMITTER + בדוק refs המכילים את ה־commit הזה + ה־COMMIT כלול ב־ + העתק דוא"ל + העתק שם + העתק שם ודוא"ל + מוצגים רק 100 השינויים הראשונים. כל השינויים זמינים בלשונית CHANGES. + מפתח: + הודעה + הורים + REFS + SHA + חותם: + פתח בדפדפן + עמודה + הזן הודעת commit. יש להפריד בין subject לתיאור באמצעות שורה ריקה! + SUBJECT + השווה + שינויים + COMMITS + COMMITS בצד שמאל בלבד + COMMITS בצד ימין בלבד + טיפ: ניתן לבצע cherry-pick ל־commits שנבחרו אם הצד השני הוא HEAD (דרך תפריט ההקשר). + הגדרות מאגר + תבנית COMMIT + פרמטרים מובנים: + + ${branch_name} שם ה־local branch הנוכחי. + ${files_num} מספר הקבצים ששונו + ${files} נתיבי הקבצים ששונו + ${files:N} עד N נתיבים של קבצים ששונו + ${pure_files} כמו ${files}, אך רק שמות קבצים נקיים + ${pure_files:N} כמו ${files:N}, אך ללא תיקיות + תוכן התבנית: + שם התבנית: + פעולה מותאמת אישית + ארגומנטים: + פרמטרים מובנים: + + ${REPO} נתיב המאגר + ${REMOTE} ה־remote שנבחר או ה־remote של ה־branch שנבחר + ${BRANCH} ה־branch שנבחר, ללא חלק ה־${REMOTE} עבור branches מרוחקים + ${BRANCH_FRIENDLY_NAME} שם ידידותי של ה־branch שנבחר, כולל את חלק ה־${REMOTE} עבור branches מרוחקים + ${SHA} ה־hash של ה־commit שנבחר + ${TAG} ה־tag שנבחר + ${FILE} הקובץ שנבחר, יחסי לשורש המאגר + $1, $2 ... ערכי שדות קלט + קובץ הרצה: + שדות קלט: + ערוך + שם: + תחום: + Branch + Commit + קובץ + Remote + מאגר + Tag + המתן לסיום הפעולה + כתובת דוא"ל + כתובת דוא"ל + GIT + לבקש אישור לפני עדכון אוטומטי של submodules + Fetch אוטומטי מ־remotes + דקות + סוגי Conventional Commit + Remote ברירת מחדל + מצב Merge מועדף + ISSUE TRACKER + הוסף כלל Azure DevOps + הוסף כלל Gerrit Change-Id Commit + הוסף כלל Gitee Issue + הוסף כלל Gitee Pull Request + הוסף כלל GitHub + הוסף כלל GitLab Issue + הוסף כלל GitLab Merge Request + הוסף כלל Jira + כלל חדש + ביטוי Regex לזיהוי Issue: + שם הכלל: + שיתוף כלל זה בקובץ .issuetracker + URL התוצאה: + השתמש ב־$1, $2 לגישה לערכי קבוצות ה־regex. + AI + שירות מועדף: + אם מוגדר 'שירות מועדף', SourceGit ישתמש רק בו במאגר זה. אחרת, אם זמינים מספר שירותים, יוצג תפריט הקשר לבחירת אחד מהם. + HTTP Proxy + HTTP proxy לשימוש המאגר הזה + שם משתמש + שם משתמש למאגר זה + ערוך שדות קלט של פעולה מותאמת אישית + ערך כאשר מסומן: + כאשר מסומן, ערך זה ישמש בארגומנטים של שורת הפקודה + תיאור: + ברירת מחדל: + תיקייה: + תווית: + אפשרויות: + השתמש ב־'|' כמפריד בין אפשרויות + String Formatter: + לא חובה. משמש לעיצוב מחרוזת הפלט. מתעלם כאשר הקלט ריק. השתמש ב־`${VALUE}` לייצוג מחרוזת הקלט. + המשתנים המובנים ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, ו־${TAG} זמינים גם כאן + סוג: + שימוש בשם ידידותי: + סביבות עבודה + צבע + שם + שחזר לשוניות בעת ההפעלה + המשך + זוהה commit ריק! האם להמשיך (--allow-empty)? + Stage לכל ו־Commit + Stage לנבחרים ו־Commit + זוהה commit ריק! האם להמשיך (--allow-empty) או לבצע stage אוטומטי ואז commit? + נדרשת הפעלה מחדש + יש להפעיל את האפליקציה מחדש כדי להחיל את השינויים. + עוזר Conventional Commit + Breaking Change: + Issue שנסגר: + פירוט השינויים: + תחום: + תיאור קצר: + סוג השינוי: + העתק + העתק כל הטקסט + העתק נתיב מלא + העתק נתיב + צור Branch... + מבוסס על: + בצע checkout ל־branch שייווצר + שינויים מקומיים: + שם ה־Branch החדש: + הזן שם branch. + צור Branch מקומי + דריסת branch קיים + צור Tag... + Tag חדש על: + חתימת GPG + הודעת Tag: + לא חובה. + שם Tag: + פורמט מומלץ: v1.0.0-alpha + Push לכל ה־remotes לאחר היצירה + צור Tag + סוג: + annotated + lightweight + החזק Ctrl להפעלה ישירה + גזור + השלך + לא לעשות דבר + Stash והחלה מחדש + ביטול אתחול של Submodule + לכפות ביטול אתחול גם אם יש בו שינויים מקומיים. + Submodule: + מחק Branch + Branch: + פעולה זו תמחק branch מרוחק!!! + מחק מספר Branches + פעולה זו תמחק מספר branches בו זמנית. בדוק היטב לפני הביצוע! + מחק מספר Tags + מחק גם מה־remotes + פעולה זו תמחק מספר tags בו זמנית. בדוק היטב לפני הביצוע! + מחק Remote + Remote: + נתיב: + יעד: + כל הצאצאים יוסרו מהרשימה. + פעולה זו רק תסיר מהרשימה, לא תמחק מהדיסק! + אישור מחיקת קבוצה + אישור מחיקת מאגר + מחק Submodule + נתיב Submodule: + מחק Tag + Tag: + מחק גם ממאגרים מרוחקים + DIFF בינארי + ההבדל הראשון + התעלמות משינויי Whitespace + מיזוג + הבדל + זה לצד זה + החלקה + ההבדל האחרון + שינוי אובייקט LFS + חדש + ההבדל הבא + אין שינויים או רק שינויי EOL + ישן + ההבדל הקודם + שמור כ־Patch + הצג סמלים מוסתרים + Diff זה לצד זה + SUBMODULE + נמחק + חדש + + {0} שינויים שלא בוצע להם commit + החלף + הדגשת תחביר + גלישת שורות + פתח בכלי Merge + הצג כל השורות + הקטנת מספר השורות הנראות + הגדלת מספר השורות הנראות + בחר קובץ להצגת השינויים + היסטוריית תיקייה + קיימים שינויים מקומיים + אינו תואם ל־upstream + כבר מעודכן + השלך שינויים + כל השינויים המקומיים ב־working copy. + שינויים: + כולל קבצים שב־ignore + כולל קבצים ששונו או נמחקו + כולל קבצים untracked + {0} שינויים יושלכו + לא ניתן לבטל פעולה זו!!! + ערוך תיאור ה־Branch + יעד: + סימנייה: + שם חדש: + יעד: + ערוך הקבוצה שנבחרה + ערוך המאגר שנבחר + יעד: + מאגר זה + Fetch + Fetch מכל ה־remotes + כפה דריסה של refs מקומיים + Fetch ללא tags + Remote: + Fetch של שינויים מרוחקים + Assume unchanged + פעולה מותאמת אישית + השלך... + השלך של {0} קבצים... + פתור באמצעות ${0}$ + שמור כ־Patch... + Stage + Stage של {0} קבצים + Stash... + Stash של {0} קבצים... + Unstage + Unstage של {0} קבצים + שימוש בשלי (checkout --ours) + שימוש בשלהם (checkout --theirs) + היסטוריית קובץ + שנה + תוכן + Git-Flow + Branch לפיתוח: + Feature: + קידומת Feature: + FLOW - סיים Feature + FLOW - סיים Hotfix + FLOW - סיים Release + יעד: + Squash בעת ה־merge + Hotfix: + קידומת Hotfix: + אתחול Git-Flow + השארת ה־branch + Branch ייצור: + Release: + קידומת Release: + התחל Feature... + FLOW - התחל Feature + התחל Hotfix... + FLOW - התחל Hotfix + הזן שם + התחל Release... + FLOW - התחל Release + קידומת Version Tag: + Git LFS + הוסף תבנית למעקב... + התבנית היא שם קובץ + תבנית מותאמת אישית: + הוסף תבנית למעקב ב־Git LFS + Fetch + הרץ `git lfs fetch` להורדת אובייקטי Git LFS. אינה מעדכנת את ה־working copy. + Fetch של אובייקטי LFS + התקן hooks של Git LFS + הצג Locks + אין קבצים נעולים + נעל + הצג הנעילות שלי בלבד + LFS Locks + שחרר נעילה + שחרר כל הנעילות שלי + האם לשחרר את כל הקבצים שנעלת? + שחרר נעילה בכוח + Prune + הרץ `git lfs prune` למחיקת קבצי LFS ישנים מהאחסון המקומי + Pull + הרץ `git lfs pull` להורדת כל קבצי Git LFS עבור ה־ref הנוכחי וביצוע checkout + Pull של אובייקטי LFS + Push + Push של קבצים גדולים בתור אל endpoint של Git LFS + Push של אובייקטי LFS + Remote: + מעקב אחר קבצים בשם '{0}' + מעקב אחר כל הקבצים *{0} + בחר Commit + היסטוריה + AUTHOR + זמן AUTHOR + זמן COMMIT + גרף ו־SUBJECT + SHA + הדגשות בגרף + הכול + ה־Branch הנוכחי בלבד + ה־Branch הנוכחי ו־Commits שנבחרו + Commits שנבחרו בלבד + נבחרו {0} COMMITS + הצג עמודות + החזק 'Ctrl' או 'Shift' לבחירת מספר commits. + החזק ⌘ או ⇧ לבחירת מספר commits. + טיפים: + פתח בחלון נפרד + פרטי Commit + השווה Revisions + קיצורי מקלדת + גלובלי + שכפל מאגר חדש + סגור הלשונית הנוכחית + עבור ללשונית הבאה + עבור ללשונית הקודמת + צור לשונית חדשה + פתח מאגר מקומי + פתח חלון ההעדפות + הצג תפריט נפתח של סביבת העבודה + החלף לשונית פעילה + הגדל/הקטן + מאגר + Commit לשינויים ב־staged + Commit ו־push לשינויים ב־staged + Stage לכל השינויים ו־commit + צור branch חדש + Fetch, מתחיל מיידית + מצב Dashboard (ברירת מחדל) + עבור לצאצא של ה־commit שנבחר + עבור להורה של ה־commit שנבחר + פתח command palette + מצב חיפוש commits + Pull, מתחיל מיידית + Push, מתחיל מיידית + טען את המאגר מחדש בכוח + הרחבה/כיווץ של חלונית הפרטים ב־`HISTORY` + עבור ל־'LOCAL CHANGES' + עבור ל־'HISTORY' + עבור ל־'STASHES' + השלך + Stage + Unstage + אתחול מאגר + האם להריץ `git init` בנתיב זה? + פתיחת המאגר נכשלה. סיבה: + נתיב: + Cherry-Pick בתהליך. + מעבד commit + Merge בתהליך. + מתבצע Merge + Rebase בתהליך. + נעצר ב־ + Revert בתהליך. + מבצע Revert ל־commit + Interactive Rebase + Stash והחלה מחדש של שינויים מקומיים + דלג על pre-rebase hook + על: + גרור כדי לסדר מחדש commits + Branch יעד: + העתק קישור + פתח בדפדפן + פקודות + שגיאה + הודעה + מאגרים פתוחים + לשוניות + סביבות עבודה + Merge של Branch + התאם הודעת merge + אל: + אפשרות Merge: + מקור: + תחילה שלי, אחר כך שלהם + תחילה שלהם, אחר כך שלי + שימוש בשניהם + כל הקונפליקטים נפתרו + נותרו {0} קונפליקטים + שלי + הקונפליקט הבא + הקונפליקט הקודם + תוצאה + שמור ו־Stage + שלהם + קונפליקטי Merge + להשליך שינויים שלא נשמרו? + שימוש בשלי + שימוש בשלהם + ביטול + Merge (מרובה) + Commit לכל השינויים + אסטרטגיה: + יעדים: + העבר Submodule + העבר אל: + Submodule: + העבר רכיב מאגר + בחר רכיב אב עבור: + שם: + לא + Git אינו מוגדר. עבור ל־[העדפות] והגדר אותו תחילה. + פתח + עורך ברירת מחדל (מערכת) + פתח תיקיית אחסון נתונים + פתח קובץ + פתח בכלי Merge חיצוני + פתח מאגר מקומי + סימנייה: + קבוצה: + תיקייה: + לא חובה. + צור לשונית חדשה + סגור לשונית + סגור לשוניות אחרות + סגור לשוניות מימין + העתק נתיב המאגר + ערוך + העבר לסביבת עבודה + רענן + מאגרים + הדבק + לפני {0} ימים + לפני שעה + לפני {0} שעות + הרגע + בחודש שעבר + בשנה שעברה + לפני {0} דקות + לפני {0} חודשים + לפני {0} שנים + אתמול + העדפות + AI + Prompt נוסף (השתמש ב־`-` לרשימת דרישות) + API Key + מודל + Fetch אוטומטי של model-ids זמינים + שם + הערך שהוזן הוא שם משתנה סביבה לטעינת ה־API key + שרת + מראה + גופן ברירת מחדל + רוחב Tab בעורך + גודל גופן + ברירת מחדל + עורך + גופן Monospace + ערכת נושא + דריסות ערכת נושא + שימוש בפסי גלילה המוסתרים אוטומטית + רוחב לשונית קבוע בשורת הכותרת + שימוש במסגרת חלון מקומית + כלי DIFF/MERGE + ארגומנטים ל־Diff + משתנים זמינים: $LOCAL, $REMOTE + ארגומנטים ל־Merge + משתנים זמינים: $BASE, $LOCAL, $REMOTE, $MERGED + נתיב התקנה + הזן נתיב לכלי diff/merge + כלי + כללי + בדוק עדכונים בעת ההפעלה + תבנית תאריך + תיקיות מצומצמות בעץ השינויים + שפה + Commits בהיסטוריה + הצג דף `LOCAL CHANGES` כברירת מחדל + הצג לשונית `CHANGES` בפרטי ה־commit כברירת מחדל + הצג זמן יחסי בגרף ה־commits + הצג tags בגרף ה־commits + אורך מנחה ל־Subject + 24 שעות + צור אווטאר ברירת מחדל בסגנון GitHub + GIT + הפעל CRLF אוטומטי + תיקיית clone ברירת מחדל + דוא"ל משתמש + דוא"ל משתמש גלובלי של git + Prune ל־branches מתים לאחר fetch + התעלמות מ־CR בסוף שורה ב־diff טקסט + נדרש Git (>= 2.25.1) עבור אפליקציה זו + נתיב התקנה + הפעל אימות HTTP SSL + שימוש ב־git-credential-libsecret במקום git-credential-manager + שם משתמש + שם משתמש גלובלי של git + Stash והחלה מחדש של שינויים כברירת מחדל בעת checkout או merge של branches + גרסת Git + חתימת GPG + חתימת GPG ל־commits + פורמט GPG + נתיב התקנת התוכנה + הזן נתיב לתוכנת gpg מותקנת + חתימת GPG ל־tags + מפתח חתימה של המשתמש + מפתח חתימת gpg של המשתמש + אינטגרציה + SHELL/טרמינל + ארגומנטים + השתמש ב־'.' לציון תיקיית העבודה + נתיב + Shell/טרמינל + Prune Remote + יעד: + Prune Worktrees + Prune למידע על worktrees ב־`$GIT_COMMON_DIR/worktrees` + Pull + Branch מרוחק: + אל: + שינויים מקומיים: + Remote: + Pull (Fetch ו־Merge) + שימוש ב־rebase במקום merge + Push + לוודא ש־submodules בוצע להם push + Force push + Branch מקומי: + חדש + Remote: + Revision: + Push של Revision ל־Remote + Push של שינויים ל־Remote + Branch מרוחק: + הגדר כ־tracking branch + Push לכל ה־tags + Push של Tag ל־Remote + Push לכל ה־remotes + Remote: + Tag: + Push ל־branch חדש + הזן שם ל־branch המרוחק החדש: + יציאה + Rebase של ה־Branch הנוכחי + Stash והחלה מחדש של שינויים מקומיים + דלג על pre-rebase hook + על: + הוסף Remote + ערוך Remote + שם: + שם Remote + URL של המאגר: + URL של מאגר git מרוחק + העתק URL + פעולה מותאמת אישית + מחק... + ערוך... + הפעל Auto-Fetch + Fetch + פתח בדפדפן + Prune + אישור הסרת Worktree + הפעל אפשרות `--force` + יעד: + שנה שם Branch + שם חדש: + שם ייחודי ל־branch זה + Branch: + ביטול + Fetch אוטומטי של שינויים מ־remotes... + מיון + לפי תאריך Committer + לפי שם + נקה (GC ו־Prune) + הרץ `git gc` עבור המאגר. + נקה הכול + נקה + הגדר מאגר זה + המשך + פעולות מותאמות אישית + אין פעולות מותאמות אישית + Dashboard + השלך כל השינויים + פתח בסייר הקבצים + חיפוש Branches/Tags/Submodules + נראות בגרף + לא מוגדר + הסתר בגרף ה־commits + סינון בגרף ה־commits + פריסה + אופקי + אנכי + סדר COMMITS + תאריך Commit + טופולוגי + BRANCHES מקומיים + אפשרויות נוספות... + ניווט אל HEAD + צור Branch + נקה התראות + פתח כתיקייה + פתח ב־{0} + פתח בכלים חיצוניים + REMOTES + הוסף Remote + פתור + חיפוש Commit + Author + תוכן + הודעה + נתיב + SHA + ב־Branch הנוכחי + Commits מעוטרים בלבד + First-parent בלבד + הצג דגלים + הצג commits אבודים + הצג Submodules כעץ + הצג Tags כעץ + דלג + סטטיסטיקות + SUBMODULES + הוסף Submodule + עדכון Submodule + TAGS + Tag חדש + לפי תאריך צור + לפי שם + מיון + פתח בטרמינל + הצג לוגים + בקר ב־'{0}' בדפדפן + WORKTREES + הוסף Worktree + Prune + URL של מאגר Git + Reset של ה־Branch הנוכחי ל־Revision + מצב Reset: + העבר אל: + Branch נוכחי: + Reset של Branch (ללא Checkout) + העבר אל: + Branch: + הצג בסייר הקבצים + Revert של Commit + Commit: + Commit לשינויי ה־revert + פועל. נא להמתין... + שמור + שמור בשם... + ה־Patch נשמר בהצלחה! + סרוק מאגרים + תיקיית שורש: + סרוק תיקייה מותאמת אישית אחרת + בדוק עדכונים... + גרסה חדשה של התוכנה זמינה: + גרסה נוכחית: + בדוק העדכונים נכשלה! + הורד + דלג על גרסה זו + תאריך שחרור הגרסה החדשה: + עדכון תוכנה + כרגע אין עדכונים זמינים. + הגדר Branch של Submodule + Submodule: + נוכחי: + שנה אל: + לא חובה. אם ריק, יוגדר לברירת המחדל. + הגדר Tracking Branch + Branch: + ביטול upstream + Upstream: + העתק SHA + עבור אל + מפתח SSH פרטי: + נתיב לאחסון מפתח SSH פרטי + התחל + Stash + כולל קבצים untracked + הודעה: + לא חובה. הודעה ל־stash זה + מצב: + קבצים ב־staged בלבד + גם השינויים שב־staged וגם אלו שלא יוכנסו ל־stash עבור הקבצים שנבחרו!!! + Stash לשינויים מקומיים + החל + החל שינויים + Checkout ל־Branch חדש + העתק הודעה + מחק + שמור כ־Patch... + מחק Stash + מחק: + STASHES + שינויים + STASHES + סטטיסטיקות + סקירה כללית + חודש + שבוע + AUTHORS: + COMMITS: + SUBMODULES + הוסף Submodule + BRANCH + Branch + נתיב יחסי + ביטול אתחול + Fetch ל־submodules מקוננים + היסטוריה + העבר אל + פתח מאגר + נתיב יחסי: + תיקייה יחסית לאחסון המודול. + מחק + הגדר Branch + שנה URL + סטטוס + שונה + לא אותחל + ה־revision שונה + unmerged + עדכון + URL + פרטי שינויים ב־Submodule + פתח פרטים + אישור + TAGGER + זמן + השווה בין 2 tags + השווה עם... + השווה עם HEAD + הודעה + שם + Tagger + העתק שם ה־Tag + פעולה מותאמת אישית + מחק ${0}$... + מחק {0} ה־tags שנבחרו... + Push ${0}$... + עדכון Submodules + כל ה־submodules + אתחול לפי הצורך + Submodule: + עדכון ל־remote tracking branch של ה־submodule + URL: + לוגים + נקה הכול + העתק + מחק + אזהרה + דף פתח + צור קבוצה + צור תת־קבוצה + שכפל מאגר + מחק + נתמכת גרירה ושחרור של תיקיות. נתמך קיבוץ מותאם אישית. + ערוך + העבר לקבוצה אחרת + פתח כל המאגרים + פתח מאגר + פתח טרמינל + סרוק מחדש של מאגרים בתיקיית ה־Clone ברירת מחדל + חיפוש מאגרים... + שינויים מקומיים + Git Ignore + התעלמות מכל הקבצים *{0} + התעלמות מקבצי *{0} באותה תיקייה + התעלמות מקבצים untracked בתיקייה זו + התעלמות מקובץ זה בלבד + Amend + ניתן לבצע stage לקובץ זה כעת. + נקה היסטוריה + האם לנקות את כל היסטוריית הודעות ה־commit? לא ניתן לבטל פעולה זו. + COMMIT + COMMIT ו־PUSH + תבנית/היסטוריה + הפעל אירוע לחיצה + Commit (ערוך) + Stage לכל השינויים ו־commit + פעולה זו תיצור commit על HEAD שמצבו detached. האם להמשיך? + ביצעת stage ל־{0} קבצים אך מוצגים רק {1} קבצים ({2} מסוננים החוצה). האם להמשיך? + זוהו קונפליקטים + MERGE + Merge עם כלי חיצוני + פתח כל הקונפליקטים בכלי Merge חיצוני + קונפליקטי הקובץ נפתרו + שימוש בשלי + שימוש בשלהם + כולל קבצים UNTRACKED + אין הודעות שהוזנו לאחרונה + אין תבניות commit + No-Verify + Reset ל־Author + SignOff + STAGED + UNSTAGE + UNSTAGE לכול + UNSTAGED + STAGE + STAGE לכול + הצג ASSUME UNCHANGED + תבנית: ${0}$ + סביבת עבודה: + הגדר סביבות עבודה... + WORKTREE + BRANCH + העתק נתיב + HEAD + נעל + פתח + נתיב + הסר + שחרר נעילה + כן + diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index bc6e8e95b..383ae256d 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -5,6 +5,8 @@ Tentang Tentang SourceGit + Tanggal Rilis: {0} + Catatan Rilis Klien Git GUI Opensource & Gratis Tambahkan Berkas ke Abaikan Pola: @@ -20,15 +22,19 @@ Buat Branch Baru Branch Yang Ada Asisten AI + Model BUAT ULANG Gunakan AI untuk membuat pesan commit - TERAPKAN SEBAGAI PESAN COMMIT + Gunakan Sembunyikan SourceGit + Sembunyikan Lainnya Tampilkan Semua - Patch - Berkas Patch: + Merge 3 Arah Pilih berkas .patch untuk diterapkan Abaikan perubahan whitespace + Sumber: + Berkas + Papan Klip Terapkan Patch Whitespace: Terapkan Stash @@ -50,22 +56,32 @@ 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. + Bisect sedang berjalan. Checkout commit lain, lalu tandai sebagai baik atau buruk. + Bisect sedang berjalan. Tandai commit saat ini sebagai buruk atau checkout commit lain, lalu tandai sebagai buruk. + Bisect berjalan. Tandai commit saat ini sebagai baik atau buruk dan checkout yang lain. + Bisect berjalan. Apakah HEAD saat ini baik atau buruk? Blame + Blame pada Revisi Sebelumnya + Abaikan perubahan whitespace BLAME PADA BERKAS INI TIDAK DIDUKUNG!!! Checkout ${0}$... - Bandingkan dengan ${0}$ - Bandingkan dengan Worktree + Bandingkan 2 branch yang dipilih + Bandingkan dengan... + Bandingkan dengan HEAD + Bandingkan dengan ${0}$ Salin Nama Branch + Buat PR... + Buat PR untuk upstream ${0}$... Aksi Kustom Hapus ${0}$... Hapus {0} branch yang dipilih + Sunting deskripsi ${0}$... Fast-Forward ke ${0}$ Fetch ${0}$ ke ${1}$... Git Flow - Selesaikan ${0}$ + Rebase ${0}$ pada ${1}$ secara Interaktif Merge ${0}$ ke ${1}$... Merge {0} branch yang dipilih ke saat ini Pull ${0}$ @@ -76,7 +92,6 @@ 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 @@ -90,6 +105,9 @@ Reset ke Revisi Parent Reset ke Revisi Ini Buat pesan commit + Merge (Bawaan) + Merge (Eksternal) + Reset Berkas ke ${0}$ UBAH MODE TAMPILAN Tampilkan sebagai Daftar Berkas dan Direktori Tampilkan sebagai Daftar Jalur @@ -98,17 +116,18 @@ Submodule: URL: Checkout Branch - Checkout Commit - Commit: - Peringatan: Dengan melakukan checkout commit, Head akan terlepas Perubahan Lokal: - Buang - Stash & Terapkan Ulang - Perbarui semua submodule Branch: HEAD saat ini mengandung commit yang tidak terhubung ke branch/tag manapun! Lanjutkan? + Submodule berikut perlu diperbarui:{0}Apakah Anda ingin memperbaruinya? Checkout & Fast-Forward Fast-Forward ke: + Checkout Branch dari Stash + Branch Baru: + Stash: + Checkout Commit atau Tag + Target: + Peringatan: Setelah proses ini, HEAD akan terlepas Cherry Pick Tambahkan sumber ke pesan commit Commit: @@ -120,6 +139,8 @@ Clone Repositori Remote Parameter Tambahan: Argumen tambahan untuk clone repositori. Opsional. + Markah: + Grup: Nama Lokal: Nama repositori. Opsional. Folder Parent: @@ -127,14 +148,20 @@ URL Repositori: TUTUP Editor + Branch + Branch & Tag + Aksi Kustom Repositori + Berkas Revisi Checkout Commit Cherry-Pick Commit Cherry-Pick ... Bandingkan dengan HEAD Bandingkan dengan Worktree Author + Waktu Penulis Pesan Committer + Waktu Committer SHA Subjek Aksi Kustom @@ -145,26 +172,22 @@ 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... + Ciutkan detail BERKAS Berkas LFS Cari Berkas... Submodule INFORMASI AUTHOR - CHILDREN COMMITTER Periksa ref yang mengandung commit ini COMMIT TERKANDUNG DALAM @@ -179,15 +202,31 @@ SHA Penanda Tangan: Buka di Browser + KOL + Masukkan pesan commit. Gunakan baris kosong untuk memisahkan subjek dan deskripsi! SUBJEK + Perbandingan + PERUBAHAN + COMMIT + COMMIT HANYA DI KIRI + COMMIT HANYA DI KANAN + Tips: Anda dapat melakukan cherry-pick pada commit yang dipilih jika sisi lainnya adalah HEAD (melalui menu konteks). Konfigurasi Repositori TEMPLATE COMMIT + Parameter bawaan: + + ${branch_name} Nama branch lokal saat ini. + ${files_num} Jumlah berkas yang berubah + ${files} Jalur berkas yang berubah + ${files:N} Maksimal N jalur berkas yang berubah + ${pure_files} Seperti ${files}, tetapi hanya nama berkas + ${pure_files:N} Seperti ${files:N}, tetapi tanpa folder Konten Template: Nama Template: AKSI KUSTOM Argumen: - Parameter bawaan: - + 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 @@ -211,9 +250,12 @@ Alamat Email Alamat email GIT + Tanyakan sebelum memperbarui submodule secara otomatis Fetch remote secara otomatis Menit + Tipe Conventional Commit Remote Default + Aktifkan `--recursive` saat memperbarui submodule secara otomatis Mode Merge Pilihan ISSUE TRACKER Tambah Aturan Azure DevOps @@ -246,7 +288,11 @@ Label: Opsi: Gunakan '|' sebagai pembatas untuk opsi + Pemformat String: + Opsional. Digunakan untuk memformat string keluaran. Diabaikan jika input kosong. Gunakan `${VALUE}` untuk mewakili string input. + Variabel bawaan ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, dan ${TAG} tetap tersedia di sini Jenis: + Gunakan Nama Ramah: Workspace Warna Nama @@ -254,6 +300,7 @@ LANJUTKAN Commit kosong terdeteksi! Lanjutkan (--allow-empty)? STAGE SEMUA & COMMIT + STAGE PILIHAN & COMMIT Commit kosong terdeteksi! Lanjutkan (--allow-empty) atau stage semua lalu commit? Perlu Mulai Ulang Anda perlu memulai ulang aplikasi ini untuk menerapkan perubahan. @@ -266,14 +313,13 @@ Jenis Perubahan: Salin Salin Semua Teks + Salin sebagai Patch 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 @@ -292,13 +338,17 @@ lightweight Tahan Ctrl untuk memulai langsung Potong + Buang + Jangan Lakukan Apa Pun + Stash & Terapkan Ulang De-initialize Submodule Paksa de-init meski mengandung perubahan lokal. Submodule: Hapus Branch + Apakah Anda juga ingin menghapus remote branch berikut? Branch: + Paksa hapus meskipun berisi commit yang belum di-merge 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 @@ -318,7 +368,7 @@ Tag: Hapus dari repositori remote DIFF BINARY - Mode Berkas Berubah + BERKAS KOSONG Perbedaan Pertama Abaikan Perubahan Whitespace BLEND @@ -338,6 +388,7 @@ PERUBAHAN SUBMODULE DIHAPUS BARU + + {0} Perubahan Belum Di-commit Tukar Syntax Highlighting Word Wrap Baris @@ -354,9 +405,12 @@ Semua perubahan lokal dalam working copy. Perubahan: Termasuk berkas yang diabaikan + Sertakan berkas yang diubah/dihapus Termasuk berkas yang tidak dilacak {0} perubahan akan dibuang Anda tidak dapat membatalkan aksi ini!!! + Sunting Deskripsi Branch + Target: Bookmark: Nama Baru: Target: @@ -371,6 +425,7 @@ Remote: Fetch Perubahan dari Remote Asumsikan tidak berubah + Aksi Kustom Buang... Buang {0} berkas... Selesaikan Menggunakan ${0}$ @@ -386,15 +441,25 @@ Riwayat Berkas PERUBAHAN KONTEN + Perubahan mode berkas: + Mode berkas yang dihapus: + Direktori + Dapat Dieksekusi + Mode berkas baru: + Normal + Submodule + Symlink + Tidak Diketahui Git-Flow Branch Development: Feature: Prefix Feature: + Selesaikan ${0}$ FLOW - Selesaikan Feature FLOW - Selesaikan Hotfix FLOW - Selesaikan Release Target: - Push ke remote setelah selesai + Rebase sebelum merge Squash saat merge Hotfix: Prefix Hotfix: @@ -403,10 +468,12 @@ Branch Production: Release: Prefix Release: + Mulai dari: Mulai Feature... FLOW - Mulai Feature Mulai Hotfix... FLOW - Mulai Hotfix + Nama: Masukkan nama Mulai Release... FLOW - Mulai Release @@ -426,6 +493,8 @@ Hanya tampilkan lock saya Lock LFS Unlock + Buka semua lock saya + Yakin ingin membuka lock semua berkas yang Anda kunci? Paksa Unlock Prune Jalankan `git lfs prune` untuk menghapus berkas LFS lama dari penyimpanan lokal @@ -438,16 +507,26 @@ Remote: Track berkas bernama '{0}' Track semua berkas *{0} + Pilih Commit RIWAYAT AUTHOR WAKTU AUTHOR + WAKTU COMMIT GRAFIK & SUBJEK SHA - WAKTU COMMIT + SOROTAN DALAM GRAFIK + Semua + Hanya Branch Saat Ini + Branch Saat Ini & Commit Terpilih + Hanya Commit Terpilih DIPILIH {0} COMMIT + TAMPILKAN KOLOM Tahan 'Ctrl' atau 'Shift' untuk memilih beberapa commit. Tahan ⌘ atau ⇧ untuk memilih beberapa commit. TIPS: + Buka di jendela terpisah + Detail Commit + Perbandingan Revisi Referensi Shortcut Keyboard GLOBAL Clone repositori baru @@ -455,31 +534,35 @@ Ke tab berikutnya Ke tab sebelumnya Buat tab baru + Buka repositori lokal Buka dialog Preferensi + Tampilkan menu tarik-turun workspace Ganti tab aktif + Perbesar/perkecil REPOSITORI Commit perubahan yang di-stage Commit dan push perubahan yang di-stage Stage semua perubahan dan commit + Buat branch baru Fetch, langsung dimulai Mode dashboard (Default) + Ke turunan commit yang dipilih + Ke induk commit yang dipilih + Buka palet perintah Mode pencarian commit Pull, langsung dimulai Push, langsung dimulai Paksa muat ulang repositori ini + Bentangkan/Ciutkan panel detail di `RIWAYAT` 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 + Apakah Anda ingin menjalankan perintah `git init` di jalur ini? + Gagal membuka repositori. Alasan: Jalur: Cherry-Pick sedang berjalan. Memproses commit @@ -491,13 +574,17 @@ Melakukan revert commit Interactive Rebase Stash & terapkan ulang perubahan lokal + Lewati hook pre-rebase Pada: Drag-drop untuk mengurutkan ulang commit Branch Target: Salin Link Buka di Browser + Perintah ERROR PEMBERITAHUAN + Versi Baru Tersedia + Buka Repositori Tab Workspace Merge Branch @@ -505,6 +592,26 @@ Ke: Opsi Merge: Sumber: + Menguji merge... + Merge dapat dilakukan tanpa konflik + Terjadi kesalahan yang tidak diketahui saat menguji merge + Merge akan menyebabkan konflik + Milik Saya dahulu, lalu Milik Mereka + Milik Mereka dahulu, lalu Milik Saya + GUNAKAN KEDUANYA + Semua konflik terselesaikan + {0} konflik tersisa + MILIK SAYA + Konflik Berikutnya + Konflik Sebelumnya + HASIL + SIMPAN & STAGE + MILIK MEREKA + Konflik Merge + Buang perubahan yang belum disimpan? + GUNAKAN MILIK SAYA + GUNAKAN MILIK MEREKA + URUNGKAN Merge (Beberapa) Commit semua perubahan Strategi: @@ -515,16 +622,26 @@ Pindahkan Node Repositori Pilih node parent untuk: Nama: + TIDAK Git BELUM dikonfigurasi. Silakan ke [Preferences] dan konfigurasikan terlebih dahulu. + Buka + Editor Default (Sistem) Buka Direktori Penyimpanan Data + Buka Berkas Buka di Merge Tool + Buka Repositori Lokal + Markah: + Grup: + Folder: Opsional. Buat Tab Baru - Bookmark Tutup Tab Tutup Tab Lain Tutup Tab di Kanan Salin Jalur Repositori + Sunting + Pindahkan ke Workspace + Segarkan Repositori Tempel {0} hari lalu @@ -539,14 +656,13 @@ Kemarin Preferensi AI - Prompt Analisis Diff + Prompt Tambahan (Gunakan `-` untuk mencantumkan kebutuhan Anda) API Key - Prompt Generate Subjek Model + Ambil model-id yang tersedia secara otomatis Nama Nilai yang dimasukkan adalah nama untuk memuat API key dari ENV Server - Aktifkan Streaming TAMPILAN Font Default Lebar Tab Editor @@ -560,6 +676,10 @@ Gunakan lebar tab tetap di titlebar Gunakan frame window native DIFF/MERGE TOOL + Argumen Diff + Variabel tersedia: $LOCAL, $REMOTE + Argumen Merge + Variabel tersedia: $BASE, $LOCAL, $REMOTE, $MERGED Jalur Instalasi Masukkan jalur untuk diff/merge tool Tool @@ -569,12 +689,13 @@ 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 waktu relatif pada grafik commit Tampilkan tag di grafik commit Panjang Panduan Subjek + Format 24 Jam + Gunakan nama branch ringkas pada grafik commit Generate avatar default bergaya GitHub GIT Aktifkan Auto CRLF @@ -589,6 +710,7 @@ Gunakan git-credential-libsecret alih-alih git-credential-manager Nama Pengguna Nama pengguna git global + Stash & terapkan ulang perubahan secara default saat checkout atau merge branch Versi Git PENANDATANGANAN GPG Penandatanganan GPG commit @@ -600,6 +722,8 @@ Kunci penandatanganan gpg pengguna INTEGRASI SHELL/TERMINAL + Argumen + Gunakan '.' untuk menunjukkan direktori kerja Jalur Shell/Terminal Prune Remote @@ -610,9 +734,6 @@ Remote Branch: Ke: Perubahan Lokal: - Buang - Stash & Terapkan Ulang - Perbarui semua submodule Remote: Pull (Fetch & Merge) Gunakan rebase alih-alih merge @@ -632,10 +753,17 @@ Push ke semua remote Remote: Tag: + Push ke branch BARU + Masukkan nama remote branch baru: Keluar Rebase Branch Saat Ini Stash & terapkan ulang perubahan lokal + Lewati hook pre-rebase Pada: + Menguji rebase... + Rebase dapat dilakukan tanpa konflik + Terjadi kesalahan yang tidak diketahui saat menguji rebase + Rebase akan menyebabkan konflik Tambah Remote Sunting Remote Nama: @@ -646,6 +774,7 @@ Aksi Kustom Hapus... Sunting... + Aktifkan Fetch Otomatis Fetch Buka di Browser Prune @@ -674,9 +803,12 @@ Buka di File Browser Cari Branch/Tag/Submodule Visibilitas di Grafik + Ciutkan Filter Tidak Diatur Sembunyikan di grafik commit + Bentangkan Filter Filter di grafik commit + filter aktif TATA LETAK Horizontal Vertikal @@ -688,14 +820,14 @@ Navigasi ke HEAD Buat Branch BERSIHKAN NOTIFIKASI - Hanya highlight branch saat ini + Buka sebagai Folder Buka di {0} Buka di Tool Eksternal REMOTE Tambah Remote + SELESAIKAN Cari Commit Author - Committer Konten Pesan Jalur @@ -718,7 +850,6 @@ Berdasarkan Nama Urut Buka di Terminal - Gunakan waktu relatif Lihat Log Kunjungi '{0}' di Browser WORKTREE @@ -736,7 +867,6 @@ Revert Commit Commit: Commit perubahan revert - Reword Pesan Commit Sedang berjalan. Harap tunggu... SIMPAN Simpan Sebagai... @@ -746,9 +876,11 @@ Pindai direktori kustom lain Periksa Pembaruan... Versi baru dari perangkat lunak ini tersedia: + Versi Saat Ini: Pemeriksaan pembaruan gagal! Unduh Lewati Versi Ini + Tanggal Rilis Versi Baru: Pembaruan Perangkat Lunak Saat ini tidak ada pembaruan yang tersedia. Atur Branch Submodule @@ -774,6 +906,8 @@ Perubahan staged dan unstaged dari berkas yang dipilih akan di-stash!!! Stash Perubahan Lokal Terapkan + Terapkan Perubahan + Checkout Branch Baru Salin Pesan Drop Simpan sebagai Patch... @@ -810,9 +944,15 @@ belum di-merge Perbarui URL + Detail Perubahan Submodule + BUKA DETAIL OK TAGGER WAKTU + Checkout Tag... + Bandingkan 2 tag + Bandingkan dengan... + Bandingkan dengan HEAD Pesan Nama Tagger @@ -825,8 +965,8 @@ Perbarui Submodule Semua submodule Inisialisasi sesuai kebutuhan - Telusuri submodule secara rekursif Submodule: + Perbarui submodule bertingkat Perbarui ke remote tracking branch submodule URL: Log @@ -853,6 +993,7 @@ Abaikan berkas *{0} di folder yang sama Abaikan berkas yang tidak dilacak di folder ini Abaikan hanya berkas ini + Abaikan semua berkas yang tidak dilacak di folder yang sama Amend Anda dapat stage berkas ini sekarang. Bersihkan Riwayat @@ -866,7 +1007,8 @@ 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 + MERGE + Merge dengan Alat Eksternal BUKA SEMUA KONFLIK DI MERGETOOL EKSTERNAL KONFLIK BERKAS DISELESAIKAN GUNAKAN MILIK SAYA @@ -888,9 +1030,13 @@ WORKSPACE: Konfigurasikan Workspace... WORKTREE + BRANCH Salin Jalur + HEAD Lock Buka + JALUR Hapus Unlock + YA diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index df5e6e912..46b2fa9a5 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -5,6 +5,8 @@ Informazioni Informazioni su SourceGit + Data di Rilascio: {0} + Note di Rilascio Client GUI Git open source e gratuito Aggiungi file a Ignora Pattern: @@ -20,13 +22,11 @@ Crea nuovo branch Branch esistente Assistente AI + Modello 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 Ignora modifiche agli spazi Applica Patch @@ -50,22 +50,29 @@ Biseca Annulla Cattiva - Bisecando. La HEAD corrente è buona o cattiva? Buona Salta - Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. + Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. + Bisecando. La HEAD corrente è buona o cattiva? Attribuisci + Attribuisci sulla Revisione Precedente + Ignora modifiche agli spazi L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!! Checkout ${0}$... - Confronta con ${0}$ - Confronta con Worktree + Confronta i 2 branch selezionati + Confronta con... + Confronta con HEAD Copia Nome Branch + Crea PR... + Crea PR per upstream ${0}$... Azione personalizzata Elimina ${0}$... Elimina i {0} branch selezionati + Modifica descrizione per ${0}$... Avanzamento Veloce a ${0}$ Recupera ${0}$ in ${1}$... Git Flow - Completa ${0}$ + Ribasare interattivamente ${0}$ su ${1}$ Unisci ${0}$ in ${1}$... Unisci i {0} branch selezionati in quello corrente Scarica ${0}$ @@ -74,16 +81,23 @@ Riallinea ${0}$ su ${1}$... Rinomina ${0}$... Resetta ${0}$ a ${1}$... + Passa a ${0}$ (worktree) Imposta Branch di Tracciamento... - Confronto Branch + {0} commit avanti + {0} commit avanti, {1} commit indietro + {0} commit indietro Invalido REMOTO + STATO TRACCIAMENTO URL + WORKTREE ANNULLA Ripristina la Revisione Padre Ripristina Questa Revisione Genera messaggio di commit + Merge (Integrato) + Merge (Esterno) CAMBIA MODALITÀ DI VISUALIZZAZIONE Mostra come elenco di file e cartelle Mostra come elenco di percorsi @@ -92,15 +106,10 @@ 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 Branch: Il tuo HEAD attuale contiene commit non connessi ad alcun branch/tag! Sicuro di voler continuare? + I seguenti sottomoduli devono essere aggiornati:{0}Vuoi aggiornarli? Checkout & Avanzamento Veloce Avanzamento Veloce verso: Cherry Pick @@ -139,16 +148,12 @@ 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... @@ -158,10 +163,12 @@ Sottomodulo INFORMAZIONI AUTORE - FIGLI CHI HA COMMITTATO Controlla i riferimenti che contengono questo commit IL COMMIT È CONTENUTO DA + Copia Email + Copia Nome + Copia Nome ed Email Mostra solo le prime 100 modifiche. Vedi tutte le modifiche nella scheda MODIFICHE. Chiave: MESSAGGIO @@ -170,15 +177,25 @@ SHA Firmatario: Apri nel Browser + Inserisci il messaggio di commit. Usa una riga vuota per separare oggetto e descrizione! OGGETTO + Confronto Configura Repository TEMPLATE DI COMMIT + Parametri integrati: + +${branch_name} Nome del branch locale corrente. +${files_num} Numero di file modificati +${files} Percorsi dei file modificati +${files:N} Massimo N percorsi di file modificati +${pure_files} Come ${files}, ma solo nomi file puri +${pure_files:N} Come ${files:N}, ma senza cartelle 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 @@ -202,8 +219,10 @@ Indirizzo Email Indirizzo email GIT + Chiedi prima di aggiornare automaticamente i sottomoduli Recupera automaticamente i remoti Minuto/i + Tipi di Commit Convenzionali Remoto Predefinito Modalità di Merge Preferita TRACCIAMENTO ISSUE @@ -237,6 +256,7 @@ Etichetta: Opzioni: Usa '|' come delimitatore per le opzioni + Le variabili integrate ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE} e ${TAG} rimangono disponibili qui Tipo: Spazi di Lavoro Colore @@ -263,8 +283,6 @@ Basato Su: Checkout del Branch Creato Modifiche Locali: - Scarta - Stasha e Ripristina Nome Nuovo Branch: Inserisci il nome del branch. Crea Branch Locale @@ -283,13 +301,15 @@ leggero Tieni premuto Ctrl per avviare direttamente Taglia + Scarta + Non fare nulla + Stash e Ripristina 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 @@ -309,10 +329,10 @@ Tag: Elimina dai repository remoti DIFF BINARIO - Modalità File Modificata Prima differenza Ignora Modifiche agli Spazi FUSIONE + DIFFERENZA AFFIANCATI SCORRIMENTO Ultima differenza @@ -337,6 +357,9 @@ Aumenta Numero di Righe Visibili SELEZIONA UN FILE PER VISUALIZZARE LE MODIFICHE Cronologia Cartella + Ha Modifiche Locali + Non allineato con Upstream + Già Aggiornato Scarta Modifiche Tutte le modifiche locali nella copia di lavoro. Modifiche: @@ -344,6 +367,8 @@ Includi file non tracciati Un totale di {0} modifiche saranno scartate Questa azione non può essere annullata!!! + Modifica Descrizione del Branch + Destinazione: Segnalibro: Nuovo Nome: Destinazione: @@ -358,6 +383,7 @@ Remoto: Recupera Modifiche Remote Presumi invariato + Azione Personalizzata Scarta... Scarta {0} file... Risolvi Usando ${0}$ @@ -381,7 +407,6 @@ FLOW - Completa Hotfix FLOW - Completa Rilascio Target: - Invia al remote dopo aver finito Esegui squash durante il merge Hotfix: Prefisso Hotfix: @@ -413,6 +438,8 @@ Mostra solo i miei blocchi Blocchi LFS Sblocca + Sblocca tutti i miei blocchi + Sei sicuro di voler sbloccare tutti i tuoi file bloccati? Forza Sblocco Elimina Esegui `git lfs prune` per eliminare vecchi file LFS dallo storage locale @@ -428,9 +455,9 @@ STORICO AUTORE ORA AUTORE + ORA COMMIT GRAFICO E OGGETTO SHA - ORA COMMIT {0} COMMIT SELEZIONATI Tieni premuto 'Ctrl' o 'Shift' per selezionare più commit. Tieni premuto ⌘ o ⇧ per selezionare più commit. @@ -443,13 +470,16 @@ Vai alla pagina precedente Crea una nuova pagina Apri la finestra delle preferenze + Mostra menu a tendina workspace Cambia scheda attiva + Ingrandisci/Rimpicciolisci REPOSITORY Committa le modifiche in tsage Committa e invia le modifiche in stage Fai lo stage di tutte le modifiche e committa Recupera, avvia direttamente Modalità Dashboard (Predefinita) + Apri tavolozza comandi Modalità ricerca commit Scarica, avvia direttamente Invia, avvia direttamente @@ -457,12 +487,6 @@ Passa a 'Modifiche' Passa a 'Storico' Passa a 'Stashes' - EDITOR TESTO - Chiudi il pannello di ricerca - Trova il prossimo risultato - Trova il risultato precedente - Apri con uno strumento di diff/merge esterno - Apri il pannello di ricerca Scarta Aggiungi in stage Rimuovi @@ -483,8 +507,10 @@ Branch di destinazione: Copia il Link Apri nel Browser + Comandi ERRORE AVVISO + Apri Repository Schede Workspaces Unisci Branch @@ -492,6 +518,22 @@ In: Opzione di Unione: Sorgente: + Prima Il Mio, poi Il Loro + Prima Il Loro, poi Il Mio + USA ENTRAMBI + Tutti i conflitti risolti + {0} conflitto/i rimanente/i + IL MIO + Conflitto Successivo + Conflitto Precedente + RISULTATO + SALVA E STAGE + IL LORO + Conflitti di Merge + Scartare le modifiche non salvate? + USA IL MIO + USA IL LORO + ANNULLA Unione (multipla) Commit di tutte le modifiche Strategia: @@ -502,16 +544,22 @@ Sposta Nodo Repository Seleziona nodo padre per: Nome: + NO Git NON è configurato. Prima vai su [Preferenze] per configurarlo. + Apri + Editor Predefinito (Sistema) Apri Cartella Dati App + Apri File Apri nello Strumento di Merge Opzionale. Crea Nuova Pagina - Segnalibro Chiudi Tab Chiudi Altri Tab Chiudi i Tab a Destra Copia Percorso Repository + Modifica + Sposta nel Workspace + Aggiorna Repository Incolla {0} giorni fa @@ -526,13 +574,10 @@ Ieri Preferenze AI - Analizza il Prompt Differenza Chiave API - Genera Prompt Oggetto - Modello Nome + Il valore inserito è il nome per caricare la chiave API da ENV Server - Abilita streaming ASPETTO Font Predefinito Larghezza della Tab Editor @@ -542,21 +587,28 @@ Font Monospaziato Tema Sostituzioni Tema + Usa barre di scorrimento a scomparsa automatica Usa larghezza fissa per i tab nella barra del titolo Usa cornice finestra nativa STRUMENTO DI DIFFERENZA/UNIONE + Argomenti Diff + Variabili disponibili: $LOCAL, $REMOTE + Argomenti Merge + Variabili disponibili: $BASE, $LOCAL, $REMOTE, $MERGED Percorso Installazione Inserisci il percorso per lo strumento di differenza/unione Strumento GENERALE Controlla aggiornamenti all'avvio Formato data + Abilita cartelle compatte nell'albero delle modifiche Lingua Numero massimo di commit nella cronologia - Mostra nel grafico l'orario dell'autore anziché quello del commit - Mostra i figli nei dettagli del commit + Mostra pagina `MODIFICHE LOCALI` per impostazione predefinita + Mostra scheda `MODIFICHE` nei dettagli del commit per impostazione predefinita Mostra i tag nel grafico dei commit Lunghezza Guida Oggetto + Genera avatar predefinito stile GitHub GIT Abilita Auto CRLF Cartella predefinita per cloni @@ -581,6 +633,8 @@ Chiave GPG dell'utente per la firma INTEGRAZIONE SHELL/TERMINALE + Argomenti + Usa '.' per indicare la cartella di lavoro Percorso Shell/Terminale Potatura Remota @@ -591,9 +645,6 @@ Branch Remoto: In: Modifiche Locali: - Scarta - Stasha e Riapplica - Aggiorna tutti i sottomoduli Remoto: Scarica (Recupera e Unisci) Riallineare anziché unire @@ -613,6 +664,8 @@ Invia a tutti i remoti Remoto: Tag: + Push su un NUOVO branch + Inserisci il nome del nuovo branch remoto: Esci Riallinea Branch Corrente Stasha e Riapplica modifiche locali @@ -669,14 +722,14 @@ Vai a HEAD Crea Branch CANCELLA LE NOTIFICHE - Evidenzia solo il branch corrente + Apri come Cartella Apri in {0} Apri in Strumenti Esterni REMOTI AGGIUNGI REMOTO + RISOLVI Cerca Commit Autore - Committer Contenuto Messaggio Percorso @@ -699,7 +752,6 @@ Per nome Ordina Apri nel Terminale - Usa tempo relativo Visualizza i Log Visita '{0}' nel Browser WORKTREE @@ -717,13 +769,13 @@ Ripristina Commit Commit: Commit delle modifiche di ripristino - Modifica Messaggio di Commit In esecuzione. Attendere... SALVA Salva come... La patch è stata salvata con successo! Scansiona Repository Cartella Principale: + Scansiona un'altra cartella personalizzata Controlla Aggiornamenti... È disponibile una nuova versione del software: Errore durante il controllo degli aggiornamenti! @@ -793,6 +845,9 @@ OK AUTORE TAG DATA + Confronta 2 tag + Confronta con... + Confronta con HEAD Messaggio Nome Autore @@ -800,12 +855,10 @@ Azione Personalizzata Elimina ${0}$... Elimina i {0} tag selezionati... - Unisci ${0}$ in ${1}$... Invia ${0}$... Aggiorna Sottomoduli Tutti i sottomoduli Inizializza se necessario - Ricorsivamente Sottomodulo: Aggiorna al branch di tracciamento remoto del sottomodulo URL: @@ -835,6 +888,8 @@ Ignora solo questo file Modifica Puoi aggiungere in stage questo file ora. + Cancella Cronologia + Sei sicuro di voler cancellare tutta la cronologia dei messaggi di commit? Questa azione non può essere annullata. COMMIT COMMIT E INVIA Template/Storico @@ -844,7 +899,8 @@ 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 + MERGE + APRI STRUMENTO DI MERGE ESTERNO APRI TUTTI I CONFLITTI NELLO STRUMENTO DI MERGE ESTERNO CONFLITTI NEI FILE RISOLTI USO IL MIO @@ -852,6 +908,7 @@ INCLUDI FILE NON TRACCIATI NESSUN MESSAGGIO RECENTE INSERITO NESSUN TEMPLATE DI COMMIT + No-Verify Reimposta Autore SignOff IN STAGE @@ -867,6 +924,8 @@ WORKTREE Copia Percorso Blocca + Apri Rimuovi Sblocca + diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 77d8edcc6..844e70874 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -3,407 +3,644 @@ - 概要 - SourceGitについて - オープンソース & フリーなGit GUIクライアント - ワークツリーを追加 + 情報 + SourceGit について + リリース日: {0} + リリースノート + オープンソース & フリーな Git GUI クライアント + ファイルの変更を無視 + パターン: + 登録先のファイル: + 作業ツリーを追加 場所: - ワークツリーのパスを入力してください。相対パスも使用することができます。 + この作業ツリーへのパス (相対パスも使用できます) ブランチの名前: - 任意。デフォルトでは宛先フォルダ名が使用されます。 + 省略可能 - 既定では宛先のフォルダーと同じ名前が使用されます 追跡するブランチ: - 追跡中のリモートブランチ + 追跡するリモートブランチを設定 チェックアウトする内容: 新しいブランチを作成 既存のブランチ - OpenAI アシスタント + AI アシスタント + モデル 再生成 - OpenAIを使用してコミットメッセージを生成 - コミットメッセージとして適用 - 適用 - パッチファイル: - 適用する .patchファイルを選択 + AI を使用してコミットメッセージを生成 + 使用 + SourceGit を隠す + 他を隠す + すべて表示 + 3 分割マージ + 適用する .patch ファイルを選択 空白文字の変更を無視 + 適用元: + ファイル + クリップボード パッチを適用 空白文字: スタッシュを適用 適用後に削除 インデックスの変更を復元 スタッシュ: - アーカイブ... + アーカイブ化... アーカイブの保存先: - アーカイブファイルのパスを選択 + アーカイブファイルへのパスを選択 リビジョン: - アーカイブ + アーカイブ化 SourceGit Askpass - 変更されていないとみなされるファイル - 変更されていないとみなされるファイルはありません - 更新 - バイナリファイルはサポートされていません!!! - Blame - BLAMEではこのファイルはサポートされていません!!! - ${0}$ をチェックアウトする... - ワークツリーと比較 + パスフレーズを入力: + 未変更と見なしたファイル + 未変更と見なしたファイルはありません + 画像を選択... + 再読み込み + バイナリファイルはサポートされていません!!! + 問題の発生源を特定 + 中止 + 問題あり + 問題なし + スキップ + 問題の発生源となったコミットを特定しています。別のコミットをチェックアウトし、そのコミットに問題があるかどうかを調査してください。 + 問題の発生源となったコミットを特定しています。問題があるコミットをチェックアウト・選択してください。 + 問題の発生源となったコミットを特定しています。現在のコミットに問題があるかどうかを調査したあと、別のコミットをチェックアウトしてみてください。 + 問題の発生源となったコミットを特定しています。現在の HEAD に問題はありませんか? + 著者の履歴 + 前のリビジョンの著者の履歴 + 空白文字の変更を無視 + このファイルに著者の履歴は表示できません!!! + ${0}$ をチェックアウト... + 選択した 2 つのブランチを比較 + 比較対象を選択... + HEAD と比較 + ${0}$ と比較 ブランチ名をコピー + プルリクエストを作成... + 上流の ${0}$ にプルリクエストを作成... カスタムアクション - ${0}$を削除... - 選択中の{0}個のブランチを削除 - ${0}$ へ早送りする - ${0}$ から ${1}$ へフェッチする - Git Flow - Finish ${0}$ - ${0}$ を ${1}$ にマージする... - 選択中の{0}個のブランチを現在のブランチにマージする - ${0}$ をプルする - ${0}$ を ${1}$ にプルする... - ${0}$ をプッシュする - ${0}$ を ${1}$ でリベースする... - ${0}$ をリネームする... - トラッキングブランチを設定... - ブランチの比較 + ${0}$ を削除... + 選択した {0} 個のブランチを削除... + ${0}$ の説明を編集... + ${0}$ まで早送り + ${0}$ から ${1}$ にフェッチ + Git フロー - ${0}$ を完了... + ${0}$ を ${1}$ で対話式リベース + ${0}$ を ${1}$ にマージ... + 現在のブランチに、選択した {0} 個のブランチをマージ... + ${0}$ からプル... + ${0}$ から ${1}$ にプル... + ${0}$ をプッシュ... + ${0}$ を ${1}$ でリベース... + ${0}$ の名前を変更... + ${0}$ を ${1}$ にリセット... + ${0}$ に切り替え (作業ツリー) + 追跡するブランチを設定... + ローカルに {0} コミット済み + ローカルに {0} コミット済み、リモートに {1} コミットあり + リモートに {0} コミットあり + 無効 + リモート + 状態 + 追跡対象 + URL + 作業ツリー キャンセル 親リビジョンにリセット このリビジョンにリセット コミットメッセージを生成 - 変更表示の切り替え - ファイルとディレクトリのリストを表示 - パスのリストを表示 - ファイルシステムのツリーを表示 + マージ (組み込みツール) + マージ (外部ツール) + ファイルを ${0}$ にリセット + 表示形式を変更 + ファイルとディレクトリの一覧で表示 + パスの一覧で表示 + ファイルシステムのツリーで表示 + サブモジュールの URL を変更 + サブモジュール: + URL: ブランチをチェックアウト - コミットをチェックアウト - コミット: - 警告: コミットをチェックアウトするとHEADが切断されます ローカルの変更: - 破棄 - スタッシュして再適用 ブランチ: - チェリーピック - ソースをコミットメッセージに追加 - コミット(複数可): + 現在の HEAD には、どのブランチやタグにも繋がっていないコミットが含まれています!それでも続行しますか? + これらのサブモジュールを更新する必要があります:{0}更新しますか? + チェックアウト & 早送り + 早送り先: + スタッシュからブランチをチェックアウト + 新しいブランチ: + スタッシュ: + コミットまたはタグをチェックアウト + 対象: + 警告: これを行うと、HEAD が切断されます + コミットを取り込む + 取り込み元をコミットメッセージに明記 + コミット: すべての変更をコミット メインライン: - 通常、マージをチェリーピックすることはできません。どちらのマージ元をメインラインとして扱うべきかが分からないためです。このオプションを使用すると、指定した親に対して変更を再適用する形でチェリーピックを実行できます。 - スタッシュをクリア - すべてのスタッシュをクリアします。続行しますか? + 通常、マージコミットを取り込むことはできません。どちらのマージ元をメインラインとして扱うべきかが分からないためです。このオプションを使用すると、指定した親コミットに対して、変更を再適用する形で取り込みを実行できます。 + スタッシュを消去 + すべてのスタッシュを消去します。よろしいですか? リモートリポジトリをクローン 追加の引数: - リポジトリをクローンする際の追加パラメータ(任意)。 + リポジトリをクローンする際の追加の引数 (省略可能) + ブックマーク: + グループ: ローカル名: - リポジトリの名前(任意)。 - 親フォルダ: + リポジトリの名前 (省略可能) + 親フォルダー: サブモジュールを初期化して更新 - リポジトリのURL: + リポジトリの URL: 閉じる - エディタ - コミットをチェックアウト - このコミットをチェリーピック - チェリーピック... - HEADと比較 - ワークツリーと比較 + エディター + ブランチ + ブランチ & タグ + リポジトリのカスタムアクション + リビジョンのファイル + コミットをチェックアウト... + コミットを取り込む... + 取り込む... + HEAD と比較 + 作業ツリーと比較 + 著者 + 著者の日時 + メッセージ + コミッター + コミット日時 SHA + タイトル カスタムアクション - ${0}$ にマージ + 対話式リベース + 削除... + 編集... + 親コミットに統合... + ${0}$ を ${1}$ で対話式リベース + 書き直す... + 親コミットに記録付きで統合... マージ... - コミットを戻す - 書き直す + ${0}$ を ${1}$ にプッシュ... + ${0}$ を ${1}$ でリベース... + ${0}$ を ${1}$ にリセット... + コミットを取り消す... パッチとして保存... - 親にスカッシュ 変更 + 個の変更されたファイル 変更を検索... + 詳細を折りたたむ ファイル - LFSファイル + LFS ファイル ファイルを検索... サブモジュール コミットの情報 著者 - コミッター - このコミットを含む参照を確認 - コミットが含まれるか確認 - 最初の100件の変更のみが表示されています。すべての変更は'変更'タブで確認できます。 + このコミットが含まれる参照を確認 + これらの参照にコミットが含まれています + メールアドレスをコピー + 名前をコピー + 名前 & メールアドレスをコピー + 最初の 100 件の変更のみが表示されています。'変更' タブですべての変更を確認できます。 + 鍵: メッセージ - + 親コミット 参照 SHA - ブラウザで開く + 署名者: + ブラウザーで開く + + コミットメッセージを入力してください。タイトルと説明は、空行で分けて記述してください! + タイトル + 比較 + 変更 + コミット + 左側のみに含まれるコミット + 右側のみに含まれるコミット + ヒント: もう片側が HEAD を指しているのであれば、(コンテキストメニューから) コミットを選択して取り込むことができます。 リポジトリの設定 コミットテンプレート - テンプレート内容: + 組み込みのパラメーター: + + ${branch_name} 現在のローカルブランチ名 + ${files_num} 変更されたファイル数 + ${files} 変更されたファイルへのパス + ${files:N} 変更されたファイルへのパス (最大 N 件) + ${pure_files} 変更されたファイルの名前 + ${pure_files:N} 変更されたファイルの名前 (最大 N 件) + テンプレートの内容: テンプレート名: カスタムアクション 引数: + 組み込みのパラメーター: + + ${REPO} リポジトリへのパス + ${REMOTE} 選択したリモート名、または選択したブランチのリモート名 + ${BRANCH} 選択したブランチ名 (リモートブランチの場合に ${REMOTE} の部分を含まない) + ${BRANCH_FRIENDLY_NAME} 選択したブランチの分かりやすい名前 (リモートブランチの場合に ${REMOTE} の部分を含む) + ${SHA} 選択したコミットのハッシュ + ${TAG} 選択したタグ名 + ${FILE} リポジトリのルートから辿った、選択したファイルへの相対パス + $1, $2 ... 入力プロンプトの値 実行ファイル: + 入力プロンプト: + 編集 名前: - スコープ: + 利用範囲: ブランチ コミット + ファイル + リモート リポジトリ + タグ アクションの終了を待機 - Eメールアドレス - Eメールアドレス - GIT - 自動的にリモートからフェッチ 間隔: - 分(s) - リモートの初期値 - ISSUEトラッカー - サンプルのAzure DevOpsルールを追加 - サンプルのGitee Issueルールを追加 - サンプルのGiteeプルリクエストルールを追加 - サンプルのGitHubルールを追加 - サンプルのGitLab Issueルールを追加 - サンプルのGitLabマージリクエストルールを追加 - サンプルのJiraルールを追加 + メールアドレス + E メールアドレス + Git + サブモジュールを自動更新する前に尋ねる + リモートから + 分ごとに自動フェッチ + コンベンショナルコミットの種類定義 + 既定のリモート + サブモジュールを自動更新する際は再帰的に実行 + 優先するマージ方式 + イシュートラッカー + Azure DevOps のルールを追加 + Gerrit Change-Id コミットのルールを追加 + Gitee イシューのルールを追加 + Gitee プルリクエストのルールを追加 + GitHub のルールを追加 + GitLab イシューのルールを追加 + GitLab マージリクエストのルールを追加 + Jira のルールを追加 新しいルール - Issueの正規表現: + イシューの正規表現: ルール名: - リザルトURL: - 正規表現のグループ値に$1, $2を使用してください。 + このルールを .issuetracker ファイルで共有 + 最終的な URL: + 正規表現のグループ値は $1, $2 で取得してください。 AI 優先するサービス: - 優先するサービスが設定されている場合、SourceGitはこのリポジトリでのみそれを使用します。そうでない場合で複数サービスが利用できる場合は、そのうちの1つを選択するためのコンテキストメニューが表示されます。 + '優先するサービス' を設定すると、このリポジトリではそのサービスのみを使用するようになります。そうでなければ、複数のサービスが存在する場合に限り、その中からひとつを選択できるコンテキストメニューが表示されます。 HTTP プロキシ - このリポジトリで使用するHTTPプロキシ + このリポジトリで使用する HTTP プロキシ ユーザー名 このリポジトリにおけるユーザー名 + カスタムアクションのプロンプトを編集 + チェック時の値: + チェックされたときにのみ、この値がコマンドライン引数として渡されます + 説明: + 既定値: + フォルダー選択: + ラベル: + 選択肢: + 選択肢は '|' で区切って記述します + 文字列の整形ツール: + 省略可能 - 最終的な出力文字列を整形するために使用されます (入力がなければ何もしません)。入力された値は `${VALUE}` で参照してください。 + ここでも組み込みの変数 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, ${TAG} がそのまま利用できます + 種類: + リモート名付きの名前を使用: ワークスペース 名前 起動時にタブを復元 - Conventional Commitヘルパー + 続行 + コミットの内容が何もありません!それでも続行しますか (--allow-empty)? + すべてステージに上げてからコミット + 選択した変更をステージに上げてからコミット + コミットの内容が何もありません!コミットする前に、変更をステージに上げますか?それともこのまま続行しますか (--allow-empty)? + 再起動が必要です + この変更は、アプリを再起動したあとに反映されます。 + コンベンショナルコミットの生成補助ツール 破壊的変更: - 閉じたIssue: + 閉じるイシュー: 詳細な変更: スコープ: - 短い説明: + 簡単な説明: 変更の種類: コピー すべてのテキストをコピー + パッチとしてコピー 絶対パスをコピー パスをコピー ブランチを作成... 派生元: 作成したブランチにチェックアウト ローカルの変更: - 破棄 - スタッシュして再適用 - 新しいブランチの名前: + 新しいブランチ名: ブランチの名前を入力 ローカルブランチを作成 + 既存のブランチを上書き タグを作成... - 付与されるコミット: - GPG署名を使用 + 付与するコミット: + GPG で署名 タグメッセージ: - 任意。 - タグの名前: - 推奨フォーマット: v1.0.0-alpha - 作成後にすべてのリモートにプッシュ - 新しいタグを作成 + 省略可能 + タグ名: + 推奨される形式: v1.0.0-alpha + タグの作成後、すべてのリモートにプッシュ + タグを作成 種類: 注釈付き 軽量 - Ctrlキーを押しながらクリックで実行 + Ctrl キーを押しながらで直接実行できます 切り取り + 破棄 + 何もしない + スタッシュして再適用 + サブモジュールの初期化を解除 + ローカルの変更の有無に関わらず、強制的に解除 + サブモジュール: ブランチを削除 + 次のリモートブランチも削除しますか? ブランチ: - リモートブランチを削除しようとしています!!! - もしリモートブランチを削除する場合、${0}$も削除します。 + 未マージのコミットがあっても強制的に削除 + リモートブランチを削除しようとしています!!! 複数のブランチを削除 - 一度に複数のブランチを削除しようとしています! 操作を行う前に再度確認してください! + 複数のブランチをまとめて削除しようとしています。操作を行う前によく確認してください! + 複数のタグを削除 + リモートからも削除 + 複数のタグをまとめて削除しようとしています。操作を行う前によく確認してください! リモートを削除 リモート: パス: 対象: - すべての子ノードがリストから削除されます。 - これはリストからのみ削除され、ディスクには保存されません! - グループを削除 - リポジトリを削除 + このグループ配下の項目も含め、すべて一覧から削除されます。 + 一覧から削除されるのみで、ディスクから削除されるわけではありません! + グループの削除を確認 + リポジトリの削除を確認 サブモジュールを削除 - サブモジュールのパス: + サブモジュールへのパス: タグを削除 タグ: - リモートリポジトリから削除 + リモートリポジトリからも削除 バイナリの差分 - ファイルモードが変更されました - 先頭の差分 - 空白の変更を無視 + 空のファイル + 最初の差分 + 空白文字の変更を無視 + ブレンド + 色差 + 左右に並べる + スライド 最後の差分 - LFSオブジェクトの変更 + LFS オブジェクトの変更 + 新版 次の差分 - 変更がない、もしくはEOLの変更のみ + 変更なし、または行末コードの変更のみ + 旧版 前の差分 パッチとして保存 - 隠されたシンボルを表示 - 差分の分割表示 + 空白文字を可視化 + 左右に並べて差分を表示 サブモジュール + 削除 新規 - スワップ - シンタックスハイライト - 行の折り返し + さらに {0} 件のコミットされていない変更があります + 入れ替え + 構文を強調表示 + 行を折り返す マージツールで開く すべての行を表示 表示する行数を減らす 表示する行数を増やす ファイルを選択すると、変更内容が表示されます + ディレクトリの履歴 + ローカルの変更あり + 上流のブランチとの相違あり + すでに最新です 変更を破棄 - ワーキングディレクトリのすべての変更を破棄 + 作業コピーに対するローカルのすべての変更 変更: - 無視したファイルを含める - {0}個の変更を破棄します。 - この操作を元に戻すことはできません!!! + 無視されたファイルを含める + 変更/削除されたファイルを含める + 未追跡のファイルを含める + {0} 件の変更が破棄されます + この操作は元に戻せません!!! + ブランチの説明を編集 + 対象: ブックマーク: 新しい名前: 対象: - 選択中のグループを編集 - 選択中のリポジトリを編集 + 選択したグループを編集 + 選択したリポジトリを編集 + 対象: + このリポジトリ フェッチ すべてのリモートをフェッチ - ローカル参照を強制的に上書き + ローカルの参照を強制的に上書き タグなしでフェッチ リモート: リモートの変更をフェッチ - 変更されていないとみなされる + 未変更と見なす + カスタムアクション 破棄... - {0}個のファイルを破棄... + {0} 個のファイルを破棄... ${0}$ を使用して解決 パッチとして保存... - ステージ - {0}個のファイルをステージ... + ステージに上げる + {0} 個のファイルをステージに上げる スタッシュ... - {0}個のファイルをスタッシュ... - アンステージ - {0}個のファイルをアンステージ... + {0} 個のファイルをスタッシュ... + ステージから降ろす + {0} 個のファイルをステージから降ろす 自分の変更を使用 (checkout --ours) 相手の変更を使用 (checkout --theirs) ファイルの履歴 変更 - コンテンツ - Git-Flow + ファイルの内容 + ファイルモードの変更: + 削除されたファイルのモード: + ディレクトリ + 実行可能 + 新しいファイルのモード: + 通常 + サブモジュール + シンボリックリンク + 不明 + Git フロー 開発ブランチ: - Feature: - Feature プレフィックス: - FLOW - Finish Feature - FLOW - Finish Hotfix - FLOW - Finish Release + 新機能の実装: + 新機能の実装用のプレフィックス: + ${0}$ を完了 + 新機能の実装を完了 + 緊急のバグ修正を完了 + リリース作業を完了 対象: - Hotfix: - Hotfix プレフィックス: - Git-Flowを初期化 - ブランチを保持 - プロダクション ブランチ: - Release: - Release プレフィックス: - Start Feature... - FLOW - Start Feature - Start Hotfix... - FLOW - Start Hotfix + マージする前にリベース + スカッシュしてマージ + 緊急のバグ修正: + 緊急のバグ修正用のプレフィックス: + Git フローを初期化 + ブランチを維持 + 本番ブランチ: + リリース作業: + リリース作業用のプレフィックス: + 開始地点: + 新機能の実装を開始... + 新機能の実装を開始 + 緊急のバグ修正を開始... + 緊急のバグ修正を開始 + 名前: 名前を入力 - Start Release... - FLOW - Start Release - Versionタグ プレフィックス: + リリース作業を開始... + リリース作業を開始 + バージョンタグのプレフィックス: Git LFS - トラックパターンを追加... + 追跡パターンを追加... パターンをファイル名として扱う - カスタム パターン: - Git LFSにトラックパターンを追加 + カスタムパターン: + Git LFS に追跡パターンを追加 フェッチ - `git lfs fetch`を実行して、Git LFSオブジェクトをダウンロードします。ワーキングコピーは更新されません。 - LFSオブジェクトをフェッチ - Git LFSフックをインストール + `git lfs fetch` を実行し、Git LFS オブジェクトをダウンロードします。作業コピーは更新されません。 + LFS オブジェクトをフェッチ + Git LFS フックをインストール ロックを表示 - ロックされているファイルはありません + ロックされたファイルはありません ロック - 私のロックのみ表示 - LFSロック - ロック解除 - 強制的にロック解除 - 削除 - `git lfs prune`を実行して、ローカルの保存領域から古いLFSファイルを削除します。 + 自分のロックのみを表示 + LFS ロック + ロックを解除 + 自分のロックをすべて解除 + 自分でロックしたファイルをすべて解除しますか? + 強制的にロックを解除 + 掃除 + `git lfs prune` を実行し、ローカルの保存領域から古い LFS ファイルを削除します プル - `git lfs pull`を実行して、現在の参照とチェックアウトのすべてのGit LFSファイルをダウンロードします。 - LFSオブジェクトをプル + `git lfs pull` を実行し、現在の参照の Git LFS ファイルをすべてダウンロード & チェックアウトします + LFS オブジェクトをプル プッシュ - キュー内の大容量ファイルをGit LFSエンドポイントにプッシュします。 - LFSオブジェクトをプッシュ + キューにある大容量ファイルを Git LFS エンドポイントにプッシュします + LFS オブジェクトをプッシュ リモート: - {0}という名前のファイルをトラック - すべての*{0}ファイルをトラック + '{0}' という名前のファイルを追跡 + *{0} ファイルをすべて追跡 + コミットを選択 履歴 著者 - 著者時間 + 著者の日時 + コミット日時 グラフ & コミットのタイトル SHA - 日時 + グラフでの強調表示 + 全範囲 + 現在のブランチのみ + 現在のブランチ & 選択したコミット + 選択したコミットのみ {0} コミットを選択しました - 'Ctrl'キーまたは'Shift'キーを押すと、複数のコミットを選択できます。 - ⌘ または ⇧ キーを押して複数のコミットを選択します。 - TIPS: + 表示する列 + 'Ctrl' または 'Shift' キーを押しながらで、複数のコミットを選択できます。 + ⌘ または ⇧ キーを押しながらで、複数のコミットを選択できます。 + ヒント: + 別のウィンドウで開く + コミットの詳細 + リビジョンを比較 キーボードショートカットを確認 総合 - 新しくリポジトリをクローン - 現在のページを閉じる - 次のページに移動 - 前のページに移動 - 新しいページを作成 + 新しいリポジトリをクローン + 現在のタブを閉じる + 次のタブに移動 + 前のタブに移動 + 新しいタブを作成 + ローカルリポジトリを開く 設定ダイアログを開く + ワークスペースのドロップダウンメニューを表示 + アクティブなタブを切り替え + 拡大/縮小 リポジトリ - ステージ済みの変更をコミット - ステージ済みの変更をコミットしてプッシュ - 全ての変更をステージしてコミット - 直接フェッチを実行 - ダッシュボードモード (初期値) + ステージに上げた変更をコミット + ステージに上げた変更をコミットしてプッシュ + すべての変更をステージに上げてコミット + 新しいブランチを作成 + フェッチ (直接実行) + ダッシュボードモード (既定) + 選択したコミットの子コミットに移動 + 選択したコミットの親コミットに移動 + コマンドパレットを開く コミット検索モード - 直接プルを実行 - 直接プッシュを実行 + プル (直接実行) + プッシュ (直接実行) 現在のリポジトリを強制的に再読み込み - '変更'に切り替える - '履歴'に切り替える - 'スタッシュ'に切り替える - テキストエディタ - 検索パネルを閉じる - 次のマッチを検索 - 前のマッチを検索 - 検索パネルを開く + `履歴` の詳細パネルを広げる/折りたたむ + 'ローカルの変更' に切り替え + '履歴' に切り替え + 'スタッシュ' に切り替え 破棄 - ステージ - アンステージ - リポジトリの初期化 + ステージに上げる + ステージから降ろす + リポジトリを初期化 + このパスで `git init` コマンドを実行しますか? + リポジトリを開けませんでした。理由: パス: - チェリーピックが進行中です。'中止'を押すと元のHEADが復元されます。 - コミットを処理中 - マージリクエストが進行中です。'中止'を押すと元のHEADが復元されます。 - マージ中 - リベースが進行中です。'中止'を押すと元のHEADが復元されます。 - 停止しました - 元に戻す処理が進行中です。'中止'を押すと元のHEADが復元されます。 - コミットを元に戻しています - インタラクティブ リベース + 取り込み作業が進行中です。 + 取り込み対象のコミット: + マージ作業が進行中です。 + マージ対象: + リベース作業が進行中です。 + 停止地点: + 取り消し作業が進行中です。 + 取り消し対象のコミット: + 対話式リベース ローカルの変更をスタッシュして再適用 - On: + リベース前のフックを実行しない + リベース地点: + ドラッグ & ドロップでコミットを並べ替えられます 対象のブランチ: リンクをコピー - ブラウザで開く + ブラウザーで開く + コマンド エラー 通知 - ブランチのマージ - 宛先: + リポジトリを開く + タブ + ワークスペース + ブランチをマージ + マージメッセージを編集 + マージ先: マージオプション: - ソースブランチ: + マージ元: + マージができるかを確認しています... + マージは衝突せずに行えます + マージ時に何らかのエラーが発生します + マージに衝突が発生します + 自分の変更 → 相手の変更の順で適用 + 相手の変更 → 自分の変更の順で適用 + 両方を使用 + すべての衝突が解決されました + {0} 件の衝突が残っています + 自分 + 次の衝突 + 前の衝突 + 結果 + 保存 & ステージに上げる + 相手 + マージ衝突 + 保存されていない変更を破棄しますか? + 自分の変更を使用 + 相手の変更を使用 + 元に戻す マージ (複数) すべての変更をコミット - マージ戦略: + マージ方式: 対象: - リポジトリノードの移動 - 親ノードを選択: + サブモジュールを移動 + 移動先: + サブモジュール: + リポジトリを別のグループに移動 + このリポジトリの新しい所属先を選択: 名前: - Gitが設定されていません。まず[設定]に移動して設定を行ってください。 - アプリケーションデータのディレクトリを開く - マージツールで開く - 任意。 - 新しいページを開く - ブックマーク + いいえ + Git の設定が行われていません。[設定] を開いて初期設定をしてください。 + 開く + 既定のエディター (システム) + データの保管ディレクトリを開く + ファイルを開く + 外部のマージツールで開く + ローカルリポジトリを開く + ブックマーク: + グループ: + フォルダー: + 省略可能 + 新しいタブを作成 タブを閉じる 他のタブを閉じる - 右のタブを閉じる - リポジトリパスをコピー + 右側のタブを閉じる + リポジトリへのパスをコピー + 編集 + ワークスペースに移動 + 再読み込み リポジトリ 貼り付け {0} 日前 @@ -418,204 +655,260 @@ 昨日 設定 AI - 差分分析プロンプト - APIキー - タイトル生成プロンプト + 追加のプロンプト (`-` で必要事項を羅列してください) + API キー モデル + 利用できるモデル ID を自動的に取得 名前 + この値を環境変数の名前とし、そこから API キーを読み込む サーバー - ストリーミングを有効化 外観 - デフォルトのフォント - エディタのタブ幅 - フォントサイズ - デフォルト - エディタ + 既定のフォント + エディターのタブ幅 + フォントの大きさ + 既定 + エディター 等幅フォント テーマ テーマの上書き - タイトルバーの固定タブ幅を使用 - ネイティブウィンドウフレームを使用 - 差分/マージ ツール - インストール パス - 差分/マージ ツールのパスを入力 + 自動的にスクロールバーを隠す + タイトルバーに固定タブ幅を使用 + ネイティブなウィンドウフレームを使用 + 差分/マージツール + 差分時の引数 + 利用できる変数: $LOCAL, $REMOTE + マージ時の引数 + 利用できる変数: $BASE, $LOCAL, $REMOTE, $MERGED + インストールパス + 差分/マージツールへのパスを入力 ツール 総合 - 起動時にアップデートを確認 - 日時のフォーマット + 起動時に更新を確認 + 日付の書式 + 変更ツリーのフォルダー階層をまとめる 言語 コミット履歴 - グラフにコミット時間の代わりに著者の時間を表示する - コミット詳細に子コミットを表示 + 初めから `ローカルの変更` ページを表示 + 初めからコミットの詳細の `変更` タブを表示 + コミットグラフを相対時間で表記 コミットグラフにタグを表示 - コミットタイトル枠の大きさ - GIT - 自動CRLFを有効化 - デフォルトのクローンディレクトリ - ユーザー Eメールアドレス - グローバルgitのEメールアドレス - フェッチ時に--pruneを有効化 - Git (>= 2.25.1) はこのアプリで必要です - インストール パス - HTTP SSL 検証を有効にする + 適切とするコミットタイトルの長さ + 24 時間 + コミットグラフのブランチ名をまとめる + GitHub のような既定のアバターを生成 + Git + 自動 CRLF 変換を有効化 + 既定のクローンディレクトリ + ユーザーのメールアドレス + Git ユーザーのメールアドレス (グローバル設定) + フェッチ時に不要なブランチを掃除 + テキストの差分で行末の CR コードを無視 + このアプリには Git (>= 2.25.1) が必須です + インストールパス + HTTP の SSL 検証を有効化 + git-credential-manager ではなく git-credential-libsecret を使用 ユーザー名 - グローバルのgitユーザー名 - Gitバージョン + Git ユーザーの名前 (グローバル設定) + ブランチのチェックアウトやマージ時に、既定で変更をスタッシュして再適用 + Git バージョン GPG 署名 - コミットにGPG署名を行う - GPGフォーマット + コミットを GPG で署名 + GPG 形式 プログラムのインストールパス - インストールされたgpgプログラムのパスを入力 - タグにGPG署名を行う - ユーザー署名キー - ユーザーのGPG署名キー + インストールされている gpg プログラムへのパスを入力 + タグを GPG で署名 + ユーザーの署名鍵 + ユーザーの GPG 署名鍵 統合 - シェル/ターミナル + シェル/端末 + 引数 + 作業ディレクトリの明示には '.' を使用してください パス - シェル/ターミナル - リモートを削除 + シェル/端末 + リモートを掃除 対象: - 作業ツリーを削除 - `$GIT_DIR/worktrees` の作業ツリー情報を削除 + 作業ツリーを掃除 + `$GIT_COMMON_DIR/worktrees` 内の作業ツリー情報を掃除 プル - ブランチ: - 宛先: + リモートブランチ: + プル先: ローカルの変更: - 破棄 - スタッシュして再適用 リモート: プル (フェッチ & マージ) - マージの代わりにリベースを使用 + マージではなくリベースを使用 プッシュ サブモジュールがプッシュされていることを確認 強制的にプッシュ - ローカル ブランチ: + ローカルブランチ: + 新規 リモート: + リビジョン: + リビジョンをリモートにプッシュ 変更をリモートにプッシュ - リモート ブランチ: - 追跡ブランチとして設定 + リモートブランチ: + 追跡するブランチとして設定 すべてのタグをプッシュ - リモートにタグをプッシュ + タグをリモートにプッシュ すべてのリモートにプッシュ リモート: タグ: + 新しいブランチにプッシュ + 新しいリモートブランチ名を入力: 終了 現在のブランチをリベース ローカルの変更をスタッシュして再適用 - On: + リベース前のフックを実行しない + リベース地点: + リベースができるかを確認しています... + リベースは衝突せずに行えます + リベース時に何らかのエラーが発生します + リベースに衝突が発生します リモートを追加 リモートを編集 名前: - リモートの名前 - リポジトリのURL: - リモートのgitリポジトリのURL - URLをコピー + リモート名 + リポジトリの URL: + リモートの Git リポジトリの URL + URL をコピー + カスタムアクション 削除... 編集... + 自動フェッチを有効化 フェッチ - ブラウザで開く - 削除 - ワークツリーの削除を確認 + ブラウザーで開く + 掃除 + 作業ツリーの削除を確認 `--force` オプションを有効化 対象: - ブランチの名前を編集 + ブランチ名を変更 新しい名前: - このブランチにつける一意な名前 + このブランチに付ける一意な名前 ブランチ: 中止 - リモートから変更を自動取得中... - クリーンアップ(GC & Prune) - このリポジトリに対して`git gc`コマンドを実行します。 - すべてのフィルターをクリア - リポジトリの設定 - 続ける + リモートから変更を自動取得しています... + 並べ替え + コミット日時順 + 名前順 + クリーンアップ (GC & Prune) + このリポジトリに `git gc` コマンドを実行します。 + すべて消去 + 消去 + このリポジトリの設定 + 続行 カスタムアクション カスタムアクションがありません + ダッシュボード すべての変更を破棄 ファイルブラウザーで開く ブランチ/タグ/サブモジュールを検索 + グラフでの可視性 解除 - コミットグラフで非表示 - コミットグラフでフィルター + コミットグラフから隠す + コミットグラフで絞り込む レイアウト 水平 垂直 コミットの並び順 - 日時 - トポロジカルソート - ローカル ブランチ - HEADに移動 + コミット日時 + トポロジカル + ローカルブランチ + その他のオプション... + HEAD に移動 ブランチを作成 - 通知をクリア + 通知を消去 + フォルダーとして開く {0} で開く 外部ツールで開く リモート リモートを追加 + 解決 コミットを検索 著者 - コミッター + ファイルの内容 メッセージ + パス SHA 現在のブランチ - タグをツリーとして表示 + 参照付きのコミットのみ + 最初の親コミットのみ + 表示フラグ + 消失したコミットを表示 + サブモジュールをツリー形式で表示 + タグをツリー形式で表示 スキップ 統計 サブモジュール サブモジュールを追加 サブモジュールを更新 タグ - 新しいタグを作成 - 作成者日時 - 名前 - ソート - ターミナルで開く - ワークツリー - ワークツリーを追加 - 削除 - GitリポジトリのURL + タグを作成 + 作成者の日時順 + 名前順 + 並べ替え + 端末で開く + ログを表示 + ブラウザーで '{0}' を訪問 + 作業ツリー + 作業ツリーを追加 + 掃除 + Git リポジトリの URL 現在のブランチをリビジョンにリセット - リセットモード: + リセット方式: 移動先: 現在のブランチ: + ブランチをリセット (チェックアウトなし) + 移動先: + ブランチ: ファイルエクスプローラーで表示 - コミットを戻す + コミットを取り消す コミット: - コミットの変更を戻す - コミットメッセージを書き直す - 実行中です。しばらくお待ちください... + 取り消しの変更をコミット + 実行しています。お待ちください... 保存 名前を付けて保存... パッチが正常に保存されました! リポジトリをスキャン ルートディレクトリ: - 更新を確認 - 新しいバージョンのソフトウェアが利用可能です: + 任意の別ディレクトリをスキャン + 更新を確認... + このソフトウェアの新しいバージョンが利用できます: + 現在のバージョン: 更新の確認に失敗しました! ダウンロード このバージョンをスキップ + 新しいバージョンのリリース日: ソフトウェアの更新 - 利用可能なアップデートはありません - トラッキングブランチを設定 + 利用できる更新はありません。 + サブモジュールのブランチを設定 + サブモジュール: + 現在: + 変更先: + 省略可能 - 空欄で既定値を使用します + 追跡するブランチを設定 ブランチ: - 上流ブランチを解除 - 上流ブランチ: - SHAをコピー - Go to - SSH プライベートキー: - プライベートSSHキーストアのパス - スタート + 上流のブランチを解除 + 上流のブランチ: + SHA をコピー + このコミットに移動 + SSH の秘密鍵: + SSH の秘密鍵が保管されているパス + 開始 スタッシュ - 追跡されていないファイルを含める + 未追跡のファイルを含める メッセージ: - オプション. このスタッシュの情報 - ステージされた変更のみ - 選択したファイルの、ステージされた変更とステージされていない変更の両方がスタッシュされます!!! + 省略可能 - このスタッシュのメッセージ + 方式: + ステージに上げたファイルのみ + 選択したファイルの、ステージに上がっている変更とそうではない変更の両方がスタッシュされます!!! ローカルの変更をスタッシュ 適用 - 破棄 - パッチとして保存 - スタッシュを破棄 - 破棄: + 変更を適用 + 新しいブランチをチェックアウト + メッセージをコピー + 削除 + パッチとして保存... + スタッシュを削除 + 削除: スタッシュ 変更 スタッシュ @@ -627,68 +920,119 @@ コミット: サブモジュール サブモジュールを追加 + ブランチ + ブランチ 相対パス - ネストされたサブモジュールを取得する - サブモジュールのリポジトリを開く + 初期化を解除 + 入れ子になったサブモジュールも取得 + 履歴 + 移動 + リポジトリを開く 相対パス: - このモジュールを保存するフォルダの相対パス - サブモジュールを削除 + このモジュールを保存するフォルダーへの相対パス + 削除 + ブランチを設定 + URL を変更 + 状態 + 変更あり + 未初期化 + リビジョンに更新あり + 未マージ + 更新 + URL + サブモジュールの変更の詳細 + 詳細を開く OK + タグの作成者 + 日時 + タグをチェックアウト... + 2 つのタグを比較 + 比較対象を選択... + HEAD と比較 + メッセージ + 名前 + タグの作成者 + タグ名をコピー + カスタムアクション ${0}$ を削除... + 選択した {0} 個のタグを削除... ${0}$ を ${1}$ にマージ... ${0}$ をプッシュ... サブモジュールを更新 すべてのサブモジュール 必要に応じて初期化 - 再帰的に更新 サブモジュール: + 入れ子になったサブモジュールも更新 + サブモジュールのリモート追跡ブランチに更新 URL: + ログ + すべて消去 + コピー + 削除 警告 - ようこそ + ようこそページ グループを作成 サブグループを作成 - リポジトリをクローンする + リポジトリをクローン 削除 - ドラッグ & ドロップでフォルダを追加できます. グループを作成したり、変更したりできます。 + ドラッグ & ドロップでフォルダーを追加できます。また、グループの作成・編集もできます。 編集 別のグループに移動 すべてのリポジトリを開く リポジトリを開く - ターミナルを開く - デフォルトのクローンディレクトリ内のリポジトリを再スキャン + 端末を開く + 既定のクローンディレクトリ内のリポジトリを再スキャン リポジトリを検索... - 変更 - Git Ignore - すべての*{0}ファイルを無視 - 同じフォルダ内の*{0}ファイルを無視 + ローカルの変更 + 無視 + *{0} ファイルをすべて無視 + 同じフォルダーの *{0} ファイルを無視 + このフォルダーで未追跡のファイルを無視 このファイルのみを無視 - Amend - このファイルを今すぐステージできます。 + 同じフォルダーで未追跡のファイルをすべて無視 + 最後のコミットをやり直す + このファイルをステージに上げられるようになりました。 + 履歴を消去 + コミットメッセージの履歴をすべて消去しますか?この操作は元に戻せません。 コミット コミットしてプッシュ - メッセージのテンプレート/履歴 - クリックイベントをトリガー - コミット (Edit) - すべての変更をステージしてコミット - 競合が検出されました - ファイルの競合は解決されました - 追跡されていないファイルを含める + テンプレート/履歴 + クリックイベントを発動 + コミット (編集) + すべての変更をステージに上げてコミット + 切断された HEAD にコミットを作成しようとしています。それでも続行しますか? + ステージに上げた {0} 個のファイルのうち、{1} 個のファイルのみが表示されています ({2} 個のファイルは非表示状態です)。続行しますか? + 衝突が検出されました + マージ + 外部ツールでマージ + すべての衝突を外部マージツールで開く + ファイルの衝突が解決されました + 自分の変更を使用 + 相手の変更を使用 + 未追跡のファイルを含める 最近の入力メッセージはありません - コミットテンプレートはありません - サインオフ - ステージしたファイル - ステージを取り消し - すべてステージを取り消し + コミットテンプレートがありません + 検証しない + 著者をリセット + 署名の行を追記 + ステージに上げたファイル + ステージから降ろす + すべてステージから降ろす 未ステージのファイル - ステージへ移動 - すべてステージへ移動 - 変更されていないとみなしたものを表示 + ステージに上げる + すべてステージに上げる + 未変更と見なしたファイルを表示 テンプレート: ${0}$ ワークスペース: - ワークスペースを設定... - ワークツリー + ワークスペースの設定... + 作業ツリー + ブランチ パスをコピー + HEAD ロック + 開く + パス 削除 - ロック解除 + ロックを解除 + はい diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index 67d3d5ca1..5a1d7b053 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -1,7 +1,11 @@ + + + 정보 SourceGit 정보 + 배포일: {0} 릴리스 노트 오픈소스 & 무료 Git GUI 클라이언트 무시할 파일 추가 @@ -18,15 +22,19 @@ 새 브랜치 생성 기존 브랜치 AI 어시스턴트 + 모델 재생성 AI를 사용하여 커밋 메시지 생성 - 커밋 메시지로 적용 + 사용 SourceGit 숨기기 + 다른 항목 숨기기 모두 보기 - 패치 - 패치 파일: + 3방향 병합 적용할 .patch 파일을 선택하세요 공백 변경 사항 무시 + 원본: + 파일 + 클립보드 패치 적용 공백: 스태시 적용 @@ -48,32 +56,40 @@ 이진 탐색 중단 나쁨 - 이진 탐색 중. 현재 HEAD가 '좋음' 상태입니까, '나쁨' 상태입니까? 좋음 건너뛰기 - 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. + 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. + 이진 탐색 중. 현재 HEAD가 '좋음' 상태입니까, '나쁨' 상태입니까? 블레임 - ${0}$ 체크아웃... - ${0}$와(과) 비교 - 워크트리와 비교 + 이전 리비전으로 책임 추적 + 공백 변경 무시 + 이 파일은 책임 추적 기능을 지원하지 않습니다 + ${0}$(으)로 체크아웃... + 선택한 두 브랜치 비교하기 + 다음과 비교... + HEAD와 비교 + ${0}$와 비교 브랜치 이름 복사 + PR 생성 + 업스트림 ${0}$에 대한 PR 생성... 사용자 지정 작업 ${0}$ 삭제... 선택한 {0}개의 브랜치 삭제 + ${0}$ 설명 편집... ${0}$(으)로 Fast-Forward ${0}$에서 ${1}$(으)로 Fetch... Git Flow - ${0}$ 완료 + ${1}$을(를) 기반으로 ${0}$ 대화형 리베이스 ${0}$을(를) ${1}$(으)로 병합... 선택한 {0}개의 브랜치를 현재 브랜치로 병합 ${0}$ Pull ${0}$에서 ${1}$(으)로 Pull... - ${0}$ Push + ${0}$(으)로 Push ${1}$을(를) 기반으로 ${0}$ 리베이스... ${0}$ 이름 바꾸기... ${0}$을(를) ${1}$(으)로 리셋... ${0}$(워크트리)로 전환 추적 브랜치 설정... - 브랜치 비교 {0}개 커밋 앞섬 {0}개 커밋 앞섬, {1}개 커밋 뒤처짐 {0}개 커밋 뒤처짐 @@ -87,6 +103,9 @@ 부모 리비전으로 리셋 이 리비전으로 리셋 커밋 메시지 생성 + 병합(내장) + 병합(외부 도구) + 파일을 ${0}$(으)로 되돌리기 표시 모드 변경 파일 및 디렉터리 목록으로 보기 경로 목록으로 보기 @@ -95,17 +114,15 @@ 서브모듈: URL: 브랜치 체크아웃 - 커밋 체크아웃 - 커밋: - 경고: 커밋 체크아웃을 하면, HEAD가 분리됩니다(detached) 로컬 변경 사항: - 폐기 - 스태시 & 재적용 - 모든 서브모듈 업데이트 브랜치: 현재 HEAD에 브랜치/태그에 연결되지 않은 커밋이 있습니다! 계속하시겠습니까? + 다음 서브모듈을 업데이트해야 합니다:{0}업데이트하시겠습니까? 체크아웃 & Fast-Forward Fast-Forward 대상: + 스태시에서 브랜치 체크아웃 + 새로운 브랜치: + 스태시: 체리픽 커밋 메시지에 원본 추가 커밋: @@ -117,6 +134,8 @@ 원격 저장소 복제 추가 파라미터: 저장소 복제 시 추가 인수. 선택 사항. + 북마크: + 그룹: 로컬 이름: 저장소 이름. 선택 사항. 상위 폴더: @@ -124,45 +143,46 @@ 저장소 URL: 닫기 에디터 + 브랜치 + 브랜치 & 태그 + 저장소 사용자 지정 작업 + 리비전 파일 커밋 체크아웃 커밋 체리픽 체리픽... HEAD와 비교 워크트리와 비교 작성자 + 작성 시간 메시지 커밋터 + 커밋 시간 SHA 제목 사용자 지정 작업 - 커밋 삭제 대화형 리베이스 삭제(Drop)... 수정(Edit)... 부모에 합치기(Fixup)... - ${1}$을(를) 기반으로 ${0}$ 대화형 리베이스 + ${1}$을(를) 기반으로 ${0}$(을)를 대화형 리베이스 메시지 수정(Reword)... 부모에 합치기(Squash)... - ${0}$(으)로 병합 병합... ${0}$을(를) ${1}$(으)로 푸시 - ${1}$을(를) 기반으로 ${0}$ 리베이스 + ${1}$을(를) 기반으로 ${0}$(을)를 리베이스 ${0}$을(를) ${1}$(으)로 리셋 커밋 되돌리기 - 메시지 수정 패치로 저장... - 부모에 합치기 - 부모에 합치기(Fixup) 변경 사항 변경된 파일 변경 사항 검색... + 세부 정보 접기 파일 LFS 파일 파일 검색... 서브모듈 정보 작성자 - 자식 커밋터 이 커밋을 포함하는 ref 확인 커밋 포함 REF @@ -177,7 +197,15 @@ SHA 서명자: 브라우저에서 열기 + + 커밋 메시지를 입력하세요. 제목과 설명은 빈 줄로 구분하세요! 제목 + 비교 + 변경점 + 커밋 + 왼쪽에만 있는 커밋 + 오른쪽에만 있는 커밋 + 팁: 반대편이 HEAD인 경우 선택한 커밋을 체리픽할 수 있습니다(컨텍스트 메뉴 사용). 저장소 설정 커밋 템플릿 ${files_num}, ${branch_name}, ${files} 및 ${files:N} (N은 출력할 최대 파일 경로 수)을(를) 사용할 수 있습니다. @@ -185,8 +213,8 @@ 템플릿 이름: 사용자 지정 작업 인수: - 내장 파라미터: - + 내장 파라미터: + ${REPO} 저장소 경로 ${REMOTE} 선택한 원격 또는 선택한 브랜치의 원격 ${BRANCH} 선택한 브랜치 (원격 브랜치의 경우 ${REMOTE} 부분 제외) @@ -210,8 +238,10 @@ 이메일 주소 이메일 주소 GIT + 서브모듈 자동 업데이트 전에 확인 원격 자동 Fetch + Conventional Commit 유형 기본 원격 선호하는 병합 모드 이슈 트래커 @@ -245,7 +275,11 @@ 레이블: 옵션: 옵션 구분자로 '|'를 사용하세요 + 문자열 포매터: + 선택 사항. 출력 문자열 형식 지정에 사용합니다. 입력이 비어 있으면 무시됩니다. 입력 문자열은 `${VALUE}`로 표시하세요. + 여기에서도 내장 변수 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, ${TAG}를 사용할 수 있습니다 유형: + 표시용 이름 사용: 작업 공간 색상 이름 @@ -253,6 +287,7 @@ 계속 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty)? 모두 스테이징 & 커밋 + 선택 항목 스테이징 후 커밋 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty) 아니면 모두 스테이징 후 커밋하시겠습니까? 재시작 필요 변경 사항을 적용하려면 앱을 다시 시작해야 합니다. @@ -265,14 +300,13 @@ 변경 유형: 복사 전체 텍스트 복사 + 패치로 복사 전체 경로 복사 경로 복사 브랜치 생성... 기준: 생성된 브랜치로 체크아웃 로컬 변경 사항: - 폐기 - 스태시 & 재적용 새 브랜치 이름: 브랜치 이름을 입력하세요. 로컬 브랜치 생성 @@ -291,13 +325,15 @@ 경량 태그 Ctrl을 누른 채 클릭하면 바로 시작합니다 잘라내기 + 폐기 + 아무것도 하지 않기 + 스태시 & 재적용 서브모듈 초기화 해제 로컬 변경 사항이 있어도 강제로 초기화 해제합니다. 서브모듈: 브랜치 삭제 브랜치: 원격 브랜치를 삭제하려고 합니다!!! - 원격 브랜치 ${0}$도 함께 삭제 여러 브랜치 삭제 한 번에 여러 브랜치를 삭제하려고 합니다. 실행하기 전에 다시 한번 확인하세요! 여러 태그 삭제 @@ -317,7 +353,6 @@ 태그: 원격 저장소에서도 삭제 바이너리 비교 - 파일 모드 변경됨 첫 번째 차이점 공백 변경 사항 무시 혼합 @@ -337,6 +372,7 @@ 서브모듈 삭제됨 신규 + 커밋 안 한 변경 사항 + {0}개 전환 구문 강조 줄 바꿈 @@ -353,12 +389,12 @@ 작업 사본의 모든 로컬 변경 사항. 변경 사항: 무시된 파일 포함 + 수정/삭제된 파일 포함 추적하지 않는 파일 포함 {0}개의 변경 사항이 폐기됩니다 이 작업은 되돌릴 수 없습니다!!! - 커밋 삭제 - 커밋: - 새 HEAD: + 브랜치 설명 수정 + 대상: 북마크: 새 이름: 대상: @@ -373,6 +409,7 @@ 원격: 원격 변경 사항 Fetch 변경되지 않음으로 간주 + 사용자 지정 작업 폐기... {0}개 파일 폐기... ${0}$을(를) 사용하여 해결 @@ -396,7 +433,6 @@ FLOW - Hotfix 완료 FLOW - Release 완료 대상: - 완료 후 원격(들)에 푸시 병합 시 스쿼시 핫픽스: Hotfix 접두사: @@ -428,6 +464,8 @@ 내 잠금만 보기 LFS 잠금 잠금 해제 + 내 잠금 모두 해제 + 잠근 파일을 모두 해제하시겠습니까? 강제 잠금 해제 정리 로컬 저장소에서 오래된 LFS 파일을 삭제하려면 `git lfs prune`을 실행하세요 @@ -440,16 +478,26 @@ 원격: '{0}' 이름의 파일 추적 모든 *{0} 파일 추적 + 커밋 선택 히스토리 작성자 작성 시간 + 커밋 시간 그래프 & 제목 SHA - 커밋 시간 + 그래프 강조 표시 + 모두 + 현재 브랜치만 + 현재 브랜치와 선택한 커밋 + 선택한 커밋만 {0}개 커밋 선택됨 + 표시할 역 'Ctrl' 또는 'Shift' 키를 누른 채로 여러 커밋을 선택하세요. ⌘ 또는 ⇧ 키를 누른 채로 여러 커밋을 선택하세요. 팁: + 별도의 창으로 열기 + 커밋 세부 정보 + 리비전 비교 키보드 단축키 참조 전역 새 저장소 복제 @@ -457,31 +505,35 @@ 다음 탭으로 이동 이전 탭으로 이동 새 탭 만들기 + 로컬 저장소 열기 환경설정 대화상자 열기 + 작업 공간 드롭다운 메뉴 열기 활성 탭 전환 + 확대 / 축소 저장소 스테이징된 변경 사항 커밋 스테이징된 변경 사항 커밋 및 푸시 모든 변경 사항 스테이징 후 커밋 + 브랜치 생성 Fetch (바로 시작) 대시보드 모드 (기본) + 선택한 커밋의 자식으로 이동 + 선택한 커밋의 부모로 이동 + 명령 팔레트 열기 커밋 검색 모드 열기 Pull (바로 시작) 푸시 (바로 시작) 이 저장소 강제 새로고침 + 히스토리에서 세부 정보 패널 펼치기/접기 '변경 사항'으로 전환 '히스토리'로 전환 '스태시'로 전환 - 텍스트 에디터 - 검색 패널 닫기 - 다음 일치 항목 찾기 - 이전 일치 항목 찾기 - 외부 diff/merge 도구로 열기 - 검색 패널 열기 폐기 스테이지 언스테이지 저장소 초기화 + 이 경로에서 `git init` 명령을 실행하시겠습니까? + 저장소를 열지 못했습니다. 원인: 경로: 체리픽 진행 중. 커밋 처리 중 @@ -493,13 +545,16 @@ 커밋 되돌리는 중 대화형 리베이스 로컬 변경 사항 스태시 & 재적용 + pre-rebase 훅 건너뛰기 기준: 드래그 앤 드롭으로 커밋 순서 변경 대상 브랜치: 링크 복사 브라우저에서 열기 + 명령 오류 알림 + 저장소 열기 작업 공간 브랜치 병합 @@ -507,6 +562,26 @@ 대상: 병합 옵션: 소스: + 병합 가능 여부 확인 중... + 충돌 없이 병합할 수 있습니다 + 병합 테스트 중 알 수 없는 오류가 발생했습니다 + 병합 시 충돌이 발생합니다 + 내 변경 먼저, 상대 변경 나중 + 상대 변경 먼저, 내 변경 나중 + 둘 다 사용 + 모든 충돌 해결됨 + 남은 충돌 {0}개 + 내 변경 + 다음 충돌 + 이전 충돌 + 결과 + 저장 후 스테이징 + 상대 변경 + 병합 충돌 + 저장하지 않은 변경사항을 버리시겠습니까? + 내 변경 사용 + 상대 변경 사용 + 실행 취소 병합 (다중) 모든 변경 사항 커밋 전략: @@ -517,16 +592,26 @@ 저장소 노드 이동 상위 노드 선택: 이름: + 아니요 Git이 구성되지 않았습니다. [환경설정]으로 이동하여 먼저 구성하세요. + 열기 + 시스템 기본 편집기 데이터 저장 디렉터리 열기 + 파일 열기 병합 도구에서 열기 + 로컬 저장소 열기 + 북마크: + 그룹: + 폴더: 선택 사항. 새 탭 만들기 - 북마크 탭 닫기 다른 탭 닫기 오른쪽 탭 닫기 저장소 경로 복사 + 편집 + 작업 공간으로 이동 + 새로 고침 저장소 붙여넣기 {0}일 전 @@ -541,14 +626,13 @@ 어제 환경설정 AI - Diff 분석 프롬프트 + 추가 프롬프트(`-`로 요구 사항 나열) API 키 - 제목 생성 프롬프트 모델 + 사용 가능한 모델 ID 자동 가져오기 이름 입력된 값은 환경변수(ENV)에서 API 키를 불러올 이름입니다 서버 - 스트리밍 활성화 모양 기본 글꼴 에디터 탭 너비 @@ -559,8 +643,13 @@ 테마 테마 재정의 스크롤바 자동 숨기기 사용 + 제목 표시줄에서 고정 탭 너비 사용 네이티브 윈도우 프레임 사용 DIFF/MERGE 도구 + 비교 인수 + 사용 가능한 변수: $LOCAL, $REMOTE + 병합 인수 + 사용 가능한 변수: $BASE, $LOCAL, $REMOTE, $MERGED 설치 경로 diff/merge 도구 경로 입력 도구 @@ -570,12 +659,13 @@ 변경 사항 트리에서 폴더 압축 활성화 언어 히스토리 커밋 수 - 그래프에 커밋 시간 대신 작성자 시간 표시 기본으로 `로컬 변경 사항` 페이지 표시 커밋 세부 정보에서 기본으로 `변경 사항` 탭 표시 - 커밋 세부 정보에 자식 커밋 표시 + 커밋 그래프에 상대 시간 표시 커밋 그래프에 태그 표시 제목 가이드 길이 + 24시간 형식 + 커밋 그래프에서 축약 브랜치 이름 사용 GitHub 스타일 기본 아바타 생성 GIT 자동 CRLF 활성화 @@ -590,6 +680,7 @@ git-credential-manager 대신 git-credential-libsecret 사용 사용자 이름 전역 git 사용자 이름 + 브랜치 체크아웃 또는 병합 시 기본적으로 변경 사항을 스태시한 뒤 다시 적용 Git 버전 GPG 서명 커밋 GPG 서명 @@ -601,6 +692,8 @@ 사용자의 gpg 서명 키 연동 셸/터미널 + 인수 + 작업 디렉터리는 `.`으로 표시하세요 경로 셸/터미널 원격 정리 @@ -611,9 +704,6 @@ 원격 브랜치: 대상: 로컬 변경 사항: - 폐기 - 스태시 & 재적용 - 모든 서브모듈 업데이트 원격: Pull (Fetch & 병합) 병합 대신 리베이스 사용 @@ -633,10 +723,17 @@ 모든 원격에 푸시 원격: 태그: + 새 브랜치로 푸시 + 새 원격 브랜치 이름을 입력하세요: 종료 현재 브랜치 리베이스 로컬 변경 사항 스태시 & 재적용 + pre-rebase 훅 건너뛰기 기준: + 리베이스 가능 여부 확인 중... + 충돌 없이 리베이스할 수 있습니다 + 리베이스 테스트 중 알 수 없는 오류가 발생했습니다 + 리베이스 시 충돌이 발생합니다 원격 추가 원격 편집 이름: @@ -647,6 +744,7 @@ 사용자 지정 작업 삭제... 편집... + 자동 Fetch 사용 Fetch 브라우저에서 열기 정리 @@ -689,14 +787,14 @@ HEAD로 이동 브랜치 생성 알림 지우기 - 현재 브랜치만 강조 + 폴더로 열기 {0}에서 열기 외부 도구에서 열기 원격 원격 추가 + 해결 커밋 검색 작성자 - 커밋터 내용 메시지 경로 @@ -719,7 +817,6 @@ 이름 순 정렬 터미널에서 열기 - 상대 시간 사용 로그 보기 브라우저에서 '{0}' 방문 워크트리 @@ -737,7 +834,6 @@ 커밋 되돌리기 커밋: 되돌린 변경 사항 커밋 - 커밋 메시지 수정 실행 중. 잠시만 기다려주세요... 저장 다른 이름으로 저장... @@ -747,9 +843,11 @@ 다른 사용자 정의 디렉터리 스캔 업데이트 확인... 이 소프트웨어의 새 버전을 사용할 수 있습니다: + 현재 버전: 업데이트 확인 실패! 다운로드 이 버전 건너뛰기 + 새 버전 릴리스 날짜: 소프트웨어 업데이트 현재 사용 가능한 업데이트가 없습니다. 서브모듈 브랜치 설정 @@ -775,6 +873,8 @@ 선택한 파일의 스테이징된 변경 사항과 스테이징되지 않은 변경 사항이 모두 스태시됩니다!!! 로컬 변경 사항 스태시 적용 + 변경 사항 적용 + 새 브랜치 체크아웃 메시지 복사 삭제 패치로 저장... @@ -808,11 +908,17 @@ 수정됨 초기화 안 됨 리비전 변경됨 + 병합되지 않음 업데이트 URL + 서브모듈 변경 상세 + 상세 열기 확인 태그 생성자 시간 + 태그 2개 비교 + 다음과 비교... + HEAD와 비교 메시지 이름 태그 생성자 @@ -820,12 +926,10 @@ 사용자 지정 작업 ${0}$ 삭제... 선택한 {0}개의 태그 삭제... - ${0}$을(를) ${1}$(으)로 병합... ${0}$ 푸시... 서브모듈 업데이트 모든 서브모듈 필요시 초기화 - 서브모듈 재귀적으로 탐색 서브모듈: 서브모듈의 원격 추적 브랜치로 업데이트 URL: @@ -866,7 +970,8 @@ 분리된(detached) HEAD에 커밋을 생성하고 있습니다. 계속하시겠습니까? {0}개의 파일을 스테이징했지만 {1}개의 파일만 표시됩니다 ({2}개의 파일은 필터링됨). 계속하시겠습니까? 충돌 감지됨 - 외부 병합 도구 열기 + 병합 + 외부 도구로 병합 모든 충돌을 외부 병합 도구에서 열기 파일 충돌 해결됨 내 것 사용 @@ -888,9 +993,13 @@ 작업 공간: 작업 공간 설정... 워크트리 + 브랜치 경로 복사 + HEAD 잠금 열기 + 경로 제거 잠금 해제 + diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index b8681a11a..8155dc647 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -5,7 +5,12 @@ Sobre Sobre o SourceGit + Data de Lançamento + Notas de Lançamento Cliente Git GUI Livre e de Código Aberto + Adicionar Arquivo(s) ao Ignore + Padrão: + Salvar arquivo: Adicionar Worktree Localização: Caminho para este worktree. Caminho relativo é suportado. @@ -17,42 +22,69 @@ Criar Novo Branch Branch Existente Assietente IA + Modelo + Gerar novamente Utilizar IA para gerar mensagem de commit - Patch - Arquivo de Patch: + Esconder SourceGit + Mostrar Todos Selecione o arquivo .patch para aplicar Ignorar mudanças de espaço em branco Aplicar Patch Espaço em Branco: + Aplicar Stash + Apagar depois de aplicar + Restabelecer as alterações do índice + Stash: Arquivar... Salvar Arquivo Como: Selecione o caminho do arquivo de arquivo Revisão: Arquivar SourceGit Askpass + Entre com a senha ARQUIVOS CONSIDERADOS SEM ALTERAÇÕES NENHUM ARQUIVO CONSIDERADO SEM ALTERAÇÕES + Carregar imagem... Atualizar ARQUIVO BINÁRIO NÃO SUPORTADO!!! + Bisect + Abortar + Inválido + Válido + Pular + Bisect. Marcar o commit atual como válido ou inválido e checar outro commit. + Bisect. O HEAD atual é válido ou inválido? Blame - BLAME NESTE ARQUIVO NÃO É SUPORTADO!!! + Ignorar espaços em branco + BLAME NESSE ARQUIVO NÃO É SUPORTADO!!! Checkout ${0}$... - Comparar com ${0}$ - Comparar com Worktree + Comparar ambos os branches selecionados + Comparar com... + Comparar com HEAD Copiar Nome do Branch + Criar PR... + Criar PR para o upstream ${0}$... + Ação personalizada Excluir ${0}$... Excluir {0} branches selecionados + Editar descri ção para %{0}%... Fast-Forward para ${0}$ Buscar ${0}$ em ${1}$... Git Flow - Finalizar ${0}$ Mesclar ${0}$ em ${1}$... + Mesclar os {0} branches no branch atual Puxar ${0}$ Puxar ${0}$ para ${1}$... Subir ${0}$ Rebase ${0}$ em ${1}$... Renomear ${0}$... + Restaurar ${0}$ para ${1}$... + Mudar para ${0}$ (worktree) Definir Branch de Rastreamento... - Comparação de Branches + {0} commit(s) à frente + Inválido + REMOTO + ESTADO CANCELAR Resetar para Revisão Pai Resetar para Esta Revisão @@ -62,12 +94,7 @@ Exibir como Lista de Caminhos Exibir como Árvore de Sistema de Arquivos Checkout Branch - Checkout Commit - Commit: - Aviso: Ao fazer o checkout de um commit, seu Head ficará desanexado Alterações Locais: - Descartar - Stash & Reaplicar Branch: Cherry-Pick Adicionar origem à mensagem de commit @@ -94,9 +121,7 @@ SHA Ação customizada Reverter Commit - Modificar Mensagem Salvar como Patch... - Mesclar ao Commit Pai ALTERAÇÕES Buscar Alterações... ARQUIVOS @@ -113,6 +138,7 @@ REFERÊNCIAS SHA Abrir no navegador + Comparação Configurar Repositório TEMPLATE DE COMMIT Conteúdo do Template: @@ -166,8 +192,6 @@ Baseado Em: Checar o branch criado Alterações Locais: - Descartar - Guardar & Reaplicar Nome do Novo Branch: Insira o nome do branch. Criar Branch Local @@ -185,10 +209,12 @@ leve Pressione Ctrl para iniciar diretamente Recortar + Descartar + Nada + Stash & Reaplicar Excluir Branch Branch: Você está prestes a excluir uma branch remota!!! - Também excluir branch remoto ${0}$ Excluir Múltiplos Branches Você está tentando excluir vários branches de uma vez. Certifique-se de verificar antes de agir! Excluir Remoto @@ -202,7 +228,6 @@ Tag: Excluir dos repositórios remotos DIFERENÇA BINÁRIA - Modo de Arquivo Alterado Ignorar mudanças de espaço em branco MUDANÇA DE OBJETO LFS Próxima Diferença @@ -305,9 +330,9 @@ Históricos AUTOR DATA DO AUTOR + HORA DO COMMIT GRÁFICO & ASSUNTO SHA - HORA DO COMMIT SELECIONADO {0} COMMITS Segure 'Ctrl' ou 'Shift' para selecionar múltiplos commits. Segure ⌘ ou ⇧ para selecionar múltiplos commits. @@ -332,11 +357,6 @@ Alternar para 'Mudanças' Alternar para 'Históricos' Alternar para 'Stashes' - EDITOR DE TEXTO - Fechar painel de busca - Encontrar próxima correspondência - Encontrar correspondência anterior - Abrir painel de busca Descartar Preparar Despreparar @@ -365,11 +385,11 @@ Abrir na Ferramenta de Mesclagem Opcional. Criar Nova Página - Adicionar aos Favoritos Fechar Aba Fechar Outras Abas Fechar Abas à Direita Copiar Caminho do Repositório + Editar Repositórios Colar {0} dias atrás @@ -384,10 +404,7 @@ Ontem Preferências INTELIGÊNCIA ARTIFICIAL - Prompt para Analisar Diff Chave da API - Prompt para Gerar Título - Modelo Nome Servidor APARÊNCIA @@ -408,7 +425,6 @@ Verificar atualizações na inicialização Idioma Commits do Histórico - Exibir data do autor em vez da data do commit no gráfico Comprimento do Guia de Assunto GIT Habilitar Auto CRLF @@ -441,8 +457,6 @@ Branch Remoto: Para: Alterações Locais: - Descartar - Guardar & Reaplicar Remoto: Puxar (Buscar & Mesclar) Usar rebase em vez de merge @@ -508,7 +522,6 @@ ADICIONAR REMOTO Pesquisar Commit Autor - Committer Mensagem SHA Branch Atual @@ -532,7 +545,6 @@ Reverter Commit Commit: Commitar alterações de reversão - Reescrever Mensagem do Commit Executando. Por favor, aguarde... SALVAR Salvar Como... @@ -580,12 +592,10 @@ Excluir Submódulo OK Excluir ${0}$... - Mesclar ${0}$ em ${1}$... Enviar ${0}$... Atualizar Submódulos Todos os submódulos Inicializar conforme necessário - Recursivamente Submódulo: URL: Aviso diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index dafade49b..525796412 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -5,6 +5,7 @@ О программе О SourceGit + Дата выпуска: {0} Примечания выпуска Бесплатный графический клиент Git с исходным кодом Добавить файл(ы) к игнорируемым @@ -21,15 +22,19 @@ Создать новую ветку Ветку из списка Помощник OpenAI + Модель ПЕРЕСОЗДАТЬ Использовать OpenAI для создания сообщения о ревизии - ПРИМЕНИТЬ КАК СООБЩЕНИЕ РЕВИЗИИ + Использовать Скрыть SourceGit + Скрыть остальные Показать все - Исправить - Файл заплатки: + Трехстороннее слияние Выберите файл .patch для применения Игнорировать изменения пробелов + Источник: + Файл + Буфер обмена Применить заплатку Пробел: Отложить @@ -51,16 +56,21 @@ Раздвоить О Плохая - Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая? Хорошая Пропустить - Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой. + Раздвоение. Пожалуйста, переключитесь на другой, затем отметьте его как хороший или плохой. + Раздвоение. Отметить текущую ревизию как плохую или переключиться на другую, а затем отметить её как плохую. + Раздвоение. Отметить текущую ревизию хорошей или плохой и переключиться на другой. + Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая? Расследование Расследование на предыдущей редакции + Игнорировать изменения пробелов РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! Переключиться на ${0}$... - Сравнить с ${0}$ - Сравнить с рабочим каталогом + Сравнить две выбранные ветки + Сравнить с ... + Сравнить с ГОЛОВОЙ + Сравнить с ${0}$ Копировать имя ветки Создать PR... Создать PR для основной ветки ${0}$... @@ -71,6 +81,7 @@ Перемотать вперёд к ${0}$ Извлечь ${0}$ в ${1}$... Git-процесс - Завершение ${0}$ + Интерактивное перемещение ${0}$ в ${1}$ Влить ${0}$ в ${1}$... Влить {0} выделенных веток в текущую Загрузить ${0}$ @@ -81,7 +92,6 @@ Сбросить ${0}$ к ${1}$... Переключить на ${0}$ (рабочий каталог) Отслеживать ветку... - Сравнение веток {0} ревизий вперёд {0} ревизий вперёд, {1} ревизий назад {0} ревизий назад @@ -95,6 +105,9 @@ Сбросить родительскую ревизию Сбросить эту ревизию Произвести сообщение о ревизии + Слить (встроенный) + Слить (внешний) + Сбросить файл(ы) в ${0}$ ИЗМЕНИТЬ РЕЖИМ ОТОБРАЖЕНИЯ Показывать в виде списка файлов и каталогов Показывать в виде списка путей @@ -103,17 +116,18 @@ Подмодуль: URL-адрес: Переключить ветку - Переключение ревизии - Ревизия: - Предупреждение: После переключения ревизии ваша Голова (HEAD) будет отсоединена Локальные изменения: - Отклонить - Отложить и применить повторно - Обновить все подкаталоги Ветка: Ваша текущая ГОЛОВА содержит ревизию(и), не связанные ни с к какими ветками или метками! Вы хотите продолжить? + Подмодулям требуется обновление:{0}Обновить их? Переключиться и перемотать Перемотать к: + Переключить ветку из отложенного + Новая ветка: + Отложенный: + Переключить ревизию или метку + Цель: + Предупреждение: После этого ГОЛОВА будет отсоединена Частичный выбор Добавить источник для ревизии сообщения Ревизия(и): @@ -125,6 +139,8 @@ Клонировать внешний репозиторий Параметры: Аргументы git clone (необязательно) + Закладка: + Группа: Локальное имя: Имя репозитория (необязательно) Родительский каталог: @@ -132,18 +148,23 @@ Адрес репозитория: ЗАКРЫТЬ Редактор + Ветки + Ветки и метки + Пользовательские действия репозитория + Ревизия файлов Переключиться на эту ревизию Применить эту ревизию (cherry-pick) Применить несколько ревизий ... Сравнить c ГОЛОВОЙ (HEAD) Сравнить с рабочим каталогом Автор + Автор (время) Сообщение Ревизор + Ревизор (время) SHA Субъект Пользовательское действие - Бросить ревизию Интерактивное перемещение Бросить... Редактировать... @@ -151,26 +172,22 @@ Интерактивное перемещение ${0}$ в ${1}$ Изменить комментарий... Втиснуть в родительский... - Влить в ${0}$ Влить ... Выложить ${0}$ в ${1}$ Переместить ${0}$ в ${1}$ Сбросить ${0}$ к ${1}$ Отменить ревизию - Изменить комментарий Сохранить как заплатки... - Объединить с предыдущей ревизией - Исправить в родительском ИЗМЕНЕНИЯ изменённый(х) файл(ов) Найти изменения.... + Свернуть подробности ФАЙЛЫ Файл LFS Поиск файлов... Подмодуль ИНФОРМАЦИЯ АВТОР - ДОЧЕРНИЙ РЕВИЗОР (ИСПОЛНИТЕЛЬ) Найти все ветки с этой ревизией ВЕТКИ С ЭТОЙ РЕВИЗИЕЙ @@ -185,12 +202,19 @@ SHA Подписант: Открыть в браузере + СТБ Введите сообщение ревизии. Пожалуйста, используйте пустые строки для разделения субъекта и описания! СУБЪЕКТ + Сравнить + ИЗМЕНЕНИЯ + РЕВИЗИИ + РЕВИЗИИ ТОЛЬКО СЛЕВА + РЕВИЗИИ ТОЛЬКО СПРАВА + Подсказка: Если другая сторона является ГОЛОВОЙ (используя контекстное меню), вы можете использовать частично выбранные ревизии (cherry-pick). Настройка репозитория ШАБЛОН РЕВИЗИИ - Встроенные параметры: - + Встроенные параметры: + ${branch_name} Имя текущей локальной ветки ${files_num} Количество изменённых файлов ${files} Пути изменённых файлов @@ -201,8 +225,8 @@ Название: ПОЛЬЗОВАТЕЛЬСКОЕ ДЕЙСТВИЕ Аргументы: - Встроенные параметры: - + Встроенные параметры: + ${REPO} Путь репозитория ${REMOTE} Выбранная удаённая ветка ${BRANCH} Выбранная ветка, без ${REMOTE} удалённых веток @@ -226,10 +250,12 @@ Адрес электронной почты Адрес электронной почты GIT + Спрашивать перед автоматическим обновлением подмодулей. Автозагрузка изменений Минут(а/ы) Общепринятые типы ревизии Внешний репозиторий по умолчанию + Включить (--recursive) при автоматическом обновлении подмодулей Предпочтительный режим слияния ОТСЛЕЖИВАНИЕ ПРОБЛЕМ Добавить пример правила Azure DevOps @@ -262,8 +288,11 @@ Метка: Опции: Используйте разделитель «|» для опций + Строка форматирования: + Не обязательно. Использовать для форматирования выходной строки. Игнорировать пустые входные данные. Пожалуйста, используйте «${VALUE}» для представления входной строки. Встроенные переменные ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, и ${TAG} останутся здесь доступными Тип: + Используйте понятное имя: Рабочие пространства Цвет Имя @@ -271,6 +300,7 @@ ПРОДОЛЖИТЬ Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty)? Сформировать всё и зафиксировать ревизию + Сформировать выбранные и зафиксировать Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, а затем зафиксировать ревизию? Требуется перезапуск Вы должны перезапустить приложение после применения изменений. @@ -283,14 +313,13 @@ Тип изменения: Копировать Копировать весь текст + Копировать как исправление Копировать полный путь Копировать путь Создать ветку... Основан на: Переключиться на созданную ветку Локальные изменения: - Отклонить - Отложить и применить повторно Имя новой ветки: Введите имя ветки. Создать локальную ветку @@ -309,13 +338,17 @@ Простой Удерживайте Ctrl, чтобы сразу начать Вырезать + Отклонить + Ничего не делать + Отложить и примненить повторно Удалить подмодуль Принудительно удалить даже если содержит локальные изменения. Подмодуль: Удалить ветку + Вы хотите также удалить следующую удалённую ветку? Ветка: + Принудительно удалить, даже если содержит неслитые ревизии Вы собираетесь удалить внешнюю ветку!!! - Также удалите внешнюю ветку ${0}$ Удаление нескольких веток Вы пытаетесь удалить несколько веток одновременно. Обязательно перепроверьте, прежде чем предпринимать какие-либо действия! Удалить несколько меток @@ -335,7 +368,7 @@ Метка: Удалить из внешнего репозитория СРАВНЕНИЕ БИНАРНИКОВ - Режим файла изменён + ФАЙЛ ПУСТ Первое сравнение Игнорировать изменения пробелов СМЕСЬ @@ -355,6 +388,7 @@ ПОДМОДУЛЬ УДАЛЁН НОВЫЙ + + {0} Незафиксированных изменний Обмен Подсветка синтаксиса Перенос слов в строке @@ -371,12 +405,10 @@ Все локальные изменения в рабочей копии. Изменения: Включить игнорируемые файлы + Включить изменённые/удалённые файлы Включить неотслеживаемые файлы {0} изменений будут отменены Вы не можете отменить это действие!!! - Бросить ревизию - Ревизия: - Новая ГОЛОВА: Править описание ветки Цель: Закладка: @@ -395,29 +427,39 @@ Не отслеживать Пользовательское действие Отклонить... - Отклонить {0} файлов... + Отклонить файлы ({0})... Взять версию ${0}$ Сохранить как файл заплатки... Сформировать - Сформированные {0} файлы + Сформировать файлы ({0}) Отложить... - Отложить {0} файлов... + Отложить файлы ({0})... Расформировать - Несформированные {0} файлы + Расформировать файлы ({0}) Использовать мой (checkout --ours) Использовать их (checkout --theirs) История файлов ИЗМЕНИТЬ СОДЕРЖИМОЕ + Изменение режима файла: + Режим удалённого файла: + Каталог + Исполняемый + Новый режим файла: + Обычный + Подмодуль + Символическая сскла + Неизвестно Git-процесс Ветка разработчика: Свойство: Свойство префикса: + Завершение ${0}$ ПРОЦЕСС - Свойства завершения ПРОЦЕСС - Закончить исправление ПРОЦЕСС - Завершить выпуск Цель: - Выложить на удалённый(ые) после завершения + Перенос перед слиянием Втиснуть при слиянии Исправление: Префикс исправлений: @@ -426,10 +468,12 @@ Производственная ветка: Выпуск: Префикс выпуска: + Начать с: Свойство запуска... ПРОЦЕСС - Свойство запуска Запуск исправлений... ПРОЦЕСС - Запуск исправлений + Имя: Ввести имя Запуск выпуска... ПРОЦЕСС - Запуск выпуска @@ -462,17 +506,27 @@ Выложить объекты LFS Внешнее хранилище: Отслеживать файлы с именем «{0}» - Отслеживать все *{0} файлов + Отслеживать все файлы (*{0}) + Выбрать ревизию Истории АВТОР ВРЕМЯ АВТОРА + ВРЕМЯ РЕВИЗИИ ГРАФ И СУБЪЕКТ SHA - ВРЕМЯ РЕВИЗИИ - ВЫБРАННЫЕ {0} РЕВИЗИИ + ПОДСВЕТКА В ГРАФЕ + ВСЕ + Только текущая ветка + Текущая ветка и выбранные ревизии + Только выбранные ревизии + ВЫБРАНО РЕВИЗИЙ: {0} + ПОКАЗЫВАТЬ КОЛОНКИ Удерживайте Ctrl или Shift, чтобы выбрать несколько ревизий. Удерживайте ⌘ или ⇧, чтобы выбрать несколько ревизий. ПОДСКАЗКИ: + Открыть в отдельном окне + Подробности ревизии + Сравнить ревизию Справка по сочетаниям клавиш ГЛОБАЛЬНЫЕ Клонировать репозиторий @@ -480,33 +534,35 @@ Перейти на следующую вкладку Перейти на предыдущую вкладку Создать новую вкладку + Открыть локальный репозиторий Открыть диалоговое окно настроек Показать рабочее пространство в выпадющем меню Переключиться на вкладку + Масштабирование РЕПОЗИТОРИЙ Зафиксировать сформированные изменения Зафиксировать и выложить сформированные изменения Сформировать все изменения и зафиксировать + Создать новую ветку Извлечь (fetch), запускается сразу Режим доски (по умолчанию) + К дочернему выбранной ревизии + К родительскому выбранной ревизии Открыть палитру команд Режим поиска ревизий Загрузить (pull), запускается сразу Выложить (push), запускается сразу Принудительно перечитать репозиторий + Развернуть/Свернуть панель подробностей в «ИСТОРИИ» Переключить на «Изменения» Переключить на «Историю» Переключить на «Отложенные» - ТЕКСТОВЫЙ РЕДАКТОР - Закрыть панель поиска - Найти следующее совпадение - Найти предыдущее совпадение - Открыть во внешнем инструменте сравнения/слияния - Открыть панель поиска Отклонить Сформировать Расформировать Создать репозиторий + Вы действительно хотите запуститб команду «git init» по этому пути? + Не удалось открыть репозиторий. Причина: Путь: Выполняется частичный перенос ревизий (cherry-pick). Обрабтка ревизии. @@ -518,6 +574,7 @@ Выполняется отмена Интерактивное перемещение Отложить и применить повторно локальные изменения + Обходной зацеп предварительного перемещения На: Перетаскивайте для переупорядочивания ревизий Целевая ветка: @@ -526,6 +583,7 @@ Команды ОШИБКА УВЕДОМЛЕНИЕ + Доступна новая версия Открыть репозитории Вкладки Рабочие места @@ -534,6 +592,26 @@ В: Опции слияния: Источник: + Тестировать на слияние... + Слияние может быть выполнено без конфликтов + Неизвестная ошибка при тестировании слияния + Слияние вызовет конфликты + Сначала мои, а потом их + Сначала их, а потом мои + ИСПОЛЬЗОВАТЬ ОБА + Все конфликты разрешены + Осталось конфликтов: {0} + MINE + Следующий конфликт + Предыдущий конфликт + РЕЗУЛЬТАТ + СОХРАНИТЬ И СФОРМИРОВАТЬ + ИХ + Конфликты при слиянии + Отменить несохранённые изменения? + ИСПОЛЬЗОВАТЬ МОИ + ИСПОЛЬЗОВАТЬ ИХ + Отменить Влить несколько веток Зафиксировать все изменения Стратегия: @@ -544,19 +622,24 @@ Переместить репозиторий в другую группу Выбрать группу для: Имя: + Нет Git НЕ был настроен. Пожалуйста, перейдите в [Настройки] и сначала настройте его. Открыть Редактор по умолчанию (Системный) Открыть каталог данных программы Открыть файл Открыть в инструменте слияния + Открыть локальный репозиторий + Закладка: + Группа: + Каталог: Необязательно. Создать новую вкладку - Закладка Закрыть вкладку Закрыть другие вкладки Закрыть вкладки справа Копировать путь репозитория + Редактировать... Переместить в рабочее пространство Обновить Репозитории @@ -573,14 +656,13 @@ Вчера Параметры ОТКРЫТЬ ИИ - Запрос на анализ сравнения + Дополнительная подсказка (Для перечисления ваших требований используйте `-`) Ключ API - Создать запрос на тему Модель + Автоматическое получение доступных идентификаторов моделей Имя: Введённое значение — это имя для загрузки API-ключа из ENV Сервер - Разрешить потоковую передачу ВИД Шрифт по умолчанию Редактировать ширину вкладки @@ -594,6 +676,7 @@ Использовать фиксированную ширину табуляции в строке заголовка. Использовать системное окно ИНСТРУМЕНТ СРАВНЕНИЙ/СЛИЯНИЯ + Аргументы сравнения Доступны переменные: $LOCAL, $REMOTE Слить аргументы Доступны переменные: $BASE, $LOCAL, $REMOTE, $MERGED @@ -606,12 +689,13 @@ Включить компактные каталоги в дереве изменений Язык Максимальная длина истории - Показывать время автора вместо времени ревизии на графике Показывать вкладку «ЛОКАЛЬНЫЕ ИЗМЕНЕНИЯ» по умолчанию Показывать вкладку «Изменения» в сведении ревизии по умолчанию - Показать наследника в деталях комментария + Отображение относительного времени на графе ревизии Показывать метки на графике Длина темы ревизии + 24-часовой + Использовать компактные имена веток в графе ревизии Создать Github-подобный аватар по умолчанию GIT Включить автозавершение CRLF @@ -626,6 +710,7 @@ Использовать git-credential-libsecret вместо git-credential-manager Имя пользователя Общее имя пользователя git + Отложить и повторно применить изменения по умолчанию при извлечении или объединении веток Версия Git GPG ПОДПИСЬ GPG подпись ревизии @@ -649,9 +734,6 @@ Ветка внешнего репозитория: В: Локальные изменения: - Отклонить - Отложить и применить повторно - Обновить все подмодули Внешний репозиторий: Загрузить (Получить и слить) Использовать перемещение вместо слияния @@ -676,7 +758,12 @@ Выйти Перемещение текущей ветки Отложить и применить повторно локальные изменения + Обходной зацеп предварительного перемещения На: + Тестировать на перемещение... + Перемещение может быть выполнено без конфликтов + Неизвестная ошибка при тестировании перемещения + Перемещение вызовет конфликты Добавить внешний репозиторий Редактировать внешний репозиторий Имя: @@ -687,6 +774,7 @@ Пользовательские действия Удалить... Редактировать... + Включить автоизвлечение Извлечь Открыть в браузере Удалить @@ -715,9 +803,12 @@ Открыть в файловом менеджере Поиск веток, меток и подмодулей Видимость на графике + Свернуть фильтры Не установлен (по умолчанию) Скрыть в графе ревизии + Развернуть фильтры Фильтр в графе ревизии + фильтры включены РАСПОЛОЖЕНИЕ Горизонтально Вертикально @@ -729,14 +820,14 @@ Навигация по ГОЛОВЕ (HEAD) Создать ветку ОЧИСТКА УВЕДОМЛЕНИЙ - Подсвечивать только текущую ветку + Открыть как каталог Открыть в {0} Открыть в расширенном инструменте ВНЕШНИЕ РЕПОЗИТОРИИ ДОБАВИТЬ ВНЕШНИЙ РЕПОЗИТОРИЙ + РАЗРЕШИТЬ Поиск ревизии Автор - Ревизор Содержимое Сообщение Путь @@ -759,7 +850,6 @@ По имени Сортировать Открыть в терминале - Использовать относительное время Просмотр журналов Посетить '{0}' в браузере РАБОЧИЕ КАТАЛОГИ @@ -777,7 +867,6 @@ Отменить ревизию Ревизия: Отмена ревизии - Изменить комментарий ревизии Запуск. Подождите пожалуйста... СОХРАНИТЬ Сохранить как... @@ -787,9 +876,11 @@ Сканировать другой пользовательский каталог Проверить обновления... Доступна новая версия программного обеспечения: + Текущая версия: Не удалось проверить наличие обновлений! Загрузка Пропустить эту версию + Дата выпуска новой версии: Обновление ПО Сейчас нет обновлений. Установить ветку подмодуля @@ -803,9 +894,6 @@ Основная ветка: Копировать SHA Перейти - Втиснуть ГОЛОВУ (HEAD) в родительскую - Исправить ГОЛОВУ (HEAD) в родительском - В: Приватный ключ SSH: Путь хранения приватного ключа SSH ЗАПУСК @@ -818,6 +906,8 @@ Сформированные так и несформированные изменения выбранных файлов будут сохранены!!! Отложить локальные изменения Принять + Применить изменения + Переключить на новую ветку Копировать сообщение Отбросить Сохранить как заплатку... @@ -854,9 +944,15 @@ не слита Обновить URL-адрес + Подробности изменения подмодуля + ОТКРЫТЬ ПОДРОБНОСТИ ОК РАЗМЕТЧИК ВРЕМЯ + Переключить метку... + Сравнить две ветки + Сравнить с ... + Сравнить с ГОЛОВОЙ Сообщение Имя Разметчик @@ -869,8 +965,8 @@ Обновление подмодулей Все подмодули Создавать по необходимости - Рекурсивно Подмодуль: + Обновить вложенные подмодули Обновить до отслеживаемой ветки удалённого подмодуля Сетевой адрес: Журналы @@ -897,6 +993,7 @@ Игнорировать *{0} файлы в том же каталоге Игнорировать неотслеживаемые файлы в этом каталоге Игнорировать только эти файлы + Игнорировать все неотслеживаемые файлы в одном и том же каталоге Изменить Теперь вы можете сформировать этот файл. Очистить историю @@ -910,11 +1007,13 @@ Вы создаёте ревизию на отсоединённой ГОЛОВЕ. Вы хотите продолжить? Вы сформировали {0} файл(ов), но отображается только {1} файл(ов) ({2} файл(ов) отфильтровано). Вы хотите продолжить? ОБНАРУЖЕНЫ КОНФЛИКТЫ - ОТКРЫТЬ ВНЕШНИЙ ИНСТРУМЕНТ СЛИЯНИЯ + СЛИТЬ + Слить с помощью внешнего инструмента ОТКРЫТЬ ВСЕ КОНФЛИКТЫ ВО ВНЕШНЕМ ИНСТРУМЕНТЕ СЛИЯНИЯ КОНФЛИКТЫ ФАЙЛОВ РАЗРЕШЕНЫ ИСПОЛЬЗОВАТЬ МОИ ИСПОЛЬЗОВАТЬ ИХ + Фильтр изменений... ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ НЕТ ШАБЛОНОВ РЕВИЗИИ @@ -932,9 +1031,13 @@ РАБОЧЕЕ ПРОСТРАНСТВО: Настройка рабочего пространства... РАБОЧИЙ КАТАЛОГ + ВЕТКА Копировать путь + ГОЛОВА Заблокировать Открыть + ПУТЬ Удалить Разблокировать + Да diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 3dc6b1c30..5cbea045b 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -17,11 +17,9 @@ புதிய கிளையை உருவாக்கு ஏற்கனவே உள்ள கிளை செநு உதவியாளர் + மாதிரி மறு-உருவாக்கு உறுதிமொழி செய்தியை உருவாக்க செநுவைப் பயன்படுத்து - உறுதிமொழி செய்தி என இடு - ஒட்டு - ஒட்டு கோப்பு: .ஒட்டு இடுவதற்கு கோப்பைத் தேர்ந்தெடு வெள்ளைவெளி மாற்றங்களைப் புறக்கணி ஒட்டு இடு @@ -43,7 +41,6 @@ குற்றச்சாட்டு இந்த கோப்பில் குற்றம் சாட்ட ஆதரிக்கப்படவில்லை!!! ${0}$ சரிபார்... - பணிமரத்துடன் ஒப்பிடுக கிளை பெயரை நகலெடு தனிப்பயன் செயல் ${0}$ ஐ நீக்கு... @@ -59,7 +56,6 @@ மறுதளம் ${0}$ இதன்மேல் ${1}$... மறுபெயரிடு ${0}$... கண்காணிப்பு கிளையை அமை... - கிளை ஒப்பிடு விடு பெற்றோர் திருத்தத்திற்கு மீட்டமை இந்த திருத்தத்திற்கு மீட்டமை @@ -69,12 +65,7 @@ பாதை பட்டியலாகக் காட்டு கோப்பு முறைமை மரமாகக் காட்டு கிளை சரிபார் - உறுதிமொழி சரிபார் - உறுதிமொழி: - முன்னறிவிப்பு: ஒரு உறுதிமொழி சரிபார்பதன் மூலம், உங்கள் தலை பிரிக்கப்படும் உள்ளக மாற்றங்கள்: - நிராகரி - பதுக்கிவை & மீண்டும் இடு கிளை: கனி பறி உறுதிமொழி செய்திக்கு மூலத்தைச் சேர் @@ -101,12 +92,9 @@ பணிமரத்துடன் ஒப்பிடுக பாகொவ-வை தனிப்பயன் செயல் - ${0}$ இதற்கு ஒன்றிணை ஒன்றிணை ... உறுதிமொழி திரும்பபெறு - வேறுமொழி ஒட்டாக சேமி... - பெற்றோர்களில் நொறுக்கு மாற்றங்கள் மாற்றங்களைத் தேடு... கோப்புகள் @@ -115,7 +103,6 @@ துணைத்தொகுதி தகவல் ஆசிரியர் - குழந்தைகள் உறுதிமொழியாளர் இந்த உறுதிமொழிடைக் கொண்ட குறிப்புகளைச் சரிபார் உறுதிமொழி இதில் உள்ளது @@ -125,6 +112,7 @@ குறிகள் பாகொவ உலாவியில் திற + ஒப்பிடு களஞ்சியம் உள்ளமை உறுதிமொழி வளர்புரு வார்ப்புரு உள்ளடக்கம்: @@ -183,8 +171,6 @@ இதன் அடிப்படையில்: உருவாக்கப்பட்ட கிளையைப் சரிபார் உள்ளக மாற்றங்கள்: - நிராகரி - பதுக்கிவை & மீண்டும் இடு புதிய கிளை பெயர்: கிளை பெயரை உள்ளிடவும். உள்ளக கிளையை உருவாக்கு @@ -202,10 +188,11 @@ குறைந்தஎடை நேரடியாகத் தொடங்க கட்டுப்பாட்டை அழுத்திப் பிடி வெட்டு + நிராகரி + பதுக்கிவை & மீண்டும் இடு கிளையை நீக்கு கிளை: நீங்கள் ஒரு தொலை கிளையை நீக்கப் போகிறீர்கள்!!! - தொலை ${0}$ கிளையையும் நீக்கு பல கிளைகளை நீக்கு நீங்கள் ஒரே நேரத்தில் பல கிளைகளை நீக்க முயற்சிக்கிறீர்கள் நடவடிக்கை எடுப்பதற்கு முன் மீண்டும் சரிபார்! தொலையை நீக்கு @@ -222,7 +209,6 @@ குறிசொல்: தொலை களஞ்சியங்களிலிருந்து நீக்கு இருமம் வேறுபாடு - கோப்பு முறை மாற்றப்பட்டது முதல் வேறுபாடு வெள்ளைவெளி மாற்றத்தை புறக்கணி கடைசி வேறுபாடு @@ -329,9 +315,9 @@ வரலாறு ஆசிரியர் ஆசிரியர் நேரம் + உறுதிமொழி நேரம் வரைபடம் & பொருள் பாகொவ - உறுதிமொழி நேரம் தேர்ந்தெடுக்கப்பட்ட {0} உறுதிமொழிகள் பல உறுதிமொழிகளைத் தேர்ந்தெடுக்க 'கட்டுப்பாடு' அல்லது 'உயர்த்து'ஐ அழுத்திப் பிடி. பல உறுதிமொழிகளைத் தேர்ந்தெடுக்க ⌘ அல்லது ⇧ ஐ அழுத்திப் பிடி. @@ -357,11 +343,6 @@ 'மாற்றங்கள்' என்பதற்கு மாறு 'வரலாறுகள்' என்பதற்கு மாறு 'பதுகிவைத்தவை' என்பதற்கு மாறு - உரை திருத்தி - தேடல் பலகத்தை மூடு - அடுத்த பொருத்தத்தைக் கண்டறி - முந்தைய பொருத்தத்தைக் கண்டறி - தேடல் பலகத்தைத் திற நிராகரி நிலைபடுத்து நிலைநீக்கு @@ -399,11 +380,11 @@ ஒன்றிணை கருவியில் திற விருப்பத்தேர்வு. புதிய பக்கத்தை உருவாக்கு - புத்தகக்குறி மூடு தாவல் பிற தாவல்களை மூடு வலதுபுறத்தில் உள்ள தாவல்களை மூடு களஞ்சிய பாதை நகலெடு + திருத்து களஞ்சியங்கள் ஒட்டு {0} நாட்களுக்கு முன்பு @@ -418,13 +399,9 @@ நேற்று விருப்பத்தேர்வுகள் செநு - வேறுபாடு உடனடியாக பகுப்பாய்வு செய் பநிஇ திறவுகோல் - பொருள் உடனடியாக உருவாக்கு - மாதிரி பெயர் சேவையகம் - ஓடையை இயக்கு தோற்றம் இயல்புநிலை எழுத்துரு திருத்தி தாவல் அகலம் @@ -445,8 +422,6 @@ தேதி வடிவம் மொழி வரலாற்று உறுதிமொழிகள் - வரைபடத்தில் உறுதிமொழி நேரத்திற்குப் பதிலாக ஆசிரியர் நேரத்தைக் காட்டு - உறுதிமொழி விவரங்களில் குழந்தைகளைக் காட்டு உறுதிமொழி வரைபடத்தில் குறிச்சொற்களைக் காட்டு பொருள் வழிகாட்டி நீளம் அறிவிலி @@ -481,8 +456,6 @@ தொலை கிளை: இதனுள்: உள்ளக மாற்றங்கள்: - நிராகரி - பதுக்கிவை & மீண்டும் இடு தொலை: இழு (எடுத்து ஒன்றிணை) ஒன்றிணை என்பதற்குப் பதிலாக மறுதளத்தைப் பயன்படுத்து @@ -554,7 +527,6 @@ தொலையைச் சேர் உறுதிமொழி தேடு ஆசிரியர் - உறுதிமொழியாளர் செய்தி பாகொவ தற்போதைய கிளை @@ -582,7 +554,6 @@ பின்வாங்கு உறுதிமொழி உறுதிமொழி: பின்வாங்கு மாற்றங்களை உறுதிமொழி - மாறுசொல் உறுதிமொழி செய்தி இயங்குகிறது. காத்திருக்கவும்... சேமி எனச் சேமி... @@ -636,12 +607,10 @@ துணை தொகுதியை நீக்கு சரி நீக்கு ${0}$... - ${0}$ இதை ${1}$ இல் இணை... தள்ளு ${0}$... துணைத்தொகுதிகளைப் புதுப்பி அனைத்து துணைத்தொகுதிகள் தேவைக்கேற்றப துவக்கு - சுழற்சி முறையில் முகவரி: முன்னறிவிப்பு வரவேற்பு பக்கம் diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index fa15b7c4b..9cad040c7 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -17,11 +17,9 @@ Створити нову гілку Наявна гілка AI Асистент + Модель ПЕРЕГЕНЕРУВАТИ Використати AI для генерації повідомлення коміту - ЗАСТОСУВАТИ ЯК ПОВІДОМЛЕННЯ КОМІТУ - Застосувати - Файл патчу: Виберіть файл .patch для застосування Ігнорувати зміни пробілів Застосувати Патч @@ -43,8 +41,6 @@ Автор рядка ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! Перейти на ${0}$... - Порівняти з ${0}$ - Порівняти з робочим деревом Копіювати назву гілки Спеціальна дія Видалити ${0}$... @@ -60,7 +56,6 @@ Перебазувати ${0}$ на ${1}$... Перейменувати ${0}$... Встановити відстежувану гілку... - Порівняти гілки СКАСУВАТИ Скинути до батьківської ревізії Скинути до цієї ревізії @@ -70,12 +65,7 @@ Показати як список шляхів Показати як дерево файлової системи Перейти на гілку - Перейти на коміт - Коміт: - Попередження: Перехід на коміт призведе до стану "від'єднаний HEAD" Локальні зміни: - Скасувати - Сховати та Застосувати Гілка: Cherry-pick Додати джерело до повідомлення коміту @@ -102,12 +92,9 @@ Порівняти з робочим деревом SHA Спеціальна дія - Злиття в ${0}$ Злити ... Скасувати коміт - Змінити повідомлення Зберегти як патч... - Склеїти з батьківським комітом ЗМІНИ Пошук змін... ФАЙЛИ @@ -116,7 +103,6 @@ Підмодуль ІНФОРМАЦІЯ АВТОР - ДОЧІРНІ КОМІТЕР Перевірити посилання, що містять цей коміт КОМІТ МІСТИТЬСЯ В @@ -126,6 +112,7 @@ ПОСИЛАННЯ (Refs) SHA Відкрити в браузері + Порівняти Налаштування сховища ШАБЛОН КОМІТУ Зміст шаблону: @@ -188,8 +175,6 @@ На основі: Перейти на створену гілку Локальні зміни: - Скасувати - Сховати та Застосувати Назва нової гілки: Введіть назву гілки. Створити локальну гілку @@ -207,10 +192,11 @@ легкий Утримуйте Ctrl для запуску без діалогу Вирізати + Скасувати + Сховати та Застосувати Видалити гілку Гілка: Ви збираєтеся видалити віддалену гілку!!! - Також видалити віддалену гілку ${0}$ Видалити кілька гілок Ви намагаєтеся видалити кілька гілок одночасно. Перевірте ще раз перед виконанням! Видалити віддалене сховище @@ -227,7 +213,6 @@ Тег: Видалити з віддалених сховищ РІЗНИЦЯ ДЛЯ БІНАРНИХ ФАЙЛІВ - Змінено режим файлу Перша відмінність Ігнорувати зміни пробілів Остання відмінність @@ -334,9 +319,9 @@ ІСТОРІЯ АВТОР ЧАС АВТОРА + ЧАС КОМІТУ ГРАФ ТА ТЕМА SHA - ЧАС КОМІТУ ВИБРАНО {0} КОМІТІВ Утримуйте 'Ctrl' або 'Shift' для вибору кількох комітів. Утримуйте ⌘ або ⇧ для вибору кількох комітів. @@ -362,11 +347,6 @@ Перейти до 'Зміни' Перейти до 'Історія' Перейти до 'Схованки' - ТЕКСТОВИЙ РЕДАКТОР - Закрити панель пошуку - Знайти наступний збіг - Знайти попередній збіг - Відкрити панель пошуку Скасувати Індексувати Видалити з індексу @@ -404,11 +384,11 @@ Відкрити в інструменті злиття Необов'язково. Створити нову вкладку - Закладка Закрити вкладку Закрити інші вкладки Закрити вкладки праворуч Копіювати шлях до сховища + Редагувати Сховища Вставити {0} днів тому @@ -423,13 +403,9 @@ Вчора Налаштування AI - Промпт для аналізу різниці Ключ API - Промпт для генерації теми - Модель Назва Сервер - Увімкнути потокове відтворення ВИГЛЯД Шрифт за замовчуванням Ширина табуляції в редакторі @@ -450,8 +426,6 @@ Формат дати Мова Кількість комітів в історії - Показувати час автора замість часу коміту в графі - Показувати дочірні коміти в деталях Показувати теги в графі комітів Довжина лінії-орієнтира для теми GIT @@ -486,8 +460,6 @@ Віддалена гілка: В: Локальні зміни: - Скасувати - Сховати та Застосувати Віддалене сховище: Pull (Fetch & Merge) Використовувати rebase замість merge @@ -559,7 +531,6 @@ ДОДАТИ ВІДДАЛЕНЕ СХОВИЩЕ Пошук коміту Автор - Комітер Повідомлення SHA Поточна гілка @@ -587,7 +558,6 @@ Revert (Скасувати коміт) Коміт: Закомітити зміни скасування - Змінити повідомлення коміту Виконується. Будь ласка, зачекайте... ЗБЕРЕГТИ Зберегти як... @@ -641,12 +611,10 @@ Видалити підмодуль OK Видалити ${0}$... - Злиття ${0}$ в ${1}$... Надіслати ${0}$... Оновити підмодулі Усі підмодулі Ініціалізувати за потреби - Рекурсивно Підмодуль: URL: Попередження @@ -678,7 +646,6 @@ Індексувати всі зміни та закомітити Ви проіндексували {0} файл(ів), але відображено лише {1} ({2} файлів відфільтровано). Продовжити? ВИЯВЛЕНО КОНФЛІКТИ - ВІДКРИТИ ЗОВНІШНІЙ ІНСТРУМЕНТ ЗЛИТТЯ ВІДКРИТИ ВСІ КОНФЛІКТИ В ЗОВНІШНЬОМУ ІНСТРУМЕНТІ ЗЛИТТЯ КОНФЛІКТИ ФАЙЛІВ ВИРІШЕНО ВИКОРИСТАТИ МОЮ ВЕРСІЮ diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index b9a5fb63f..d77cd5418 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -5,6 +5,7 @@ 关于软件 关于本软件 + 发布日期:{0} 浏览版本更新说明 开源免费的Git客户端 新增忽略文件 @@ -21,22 +22,26 @@ 创建新分支 已有分支 AI助手 + 模型 重新生成 使用AI助手生成提交信息 - 应用本次生成 + 应用 隐藏 SourceGit - 显示所有窗口 - 应用补丁(apply) - 补丁文件 : + 隐藏其他 + 显示全部 + 尝试三路合并 选择补丁文件 忽略空白符号 + 补丁来源 : + 文件 + 剪贴板 应用补丁 空白符号处理 : 应用贮藏 在成功应用后丢弃该贮藏 恢复索引中已暂存的变化 已选贮藏 : - 存档(archive) ... + 存档(archive)... 存档文件路径: 选择存档文件的存放路径 指定的提交: @@ -51,37 +56,42 @@ 二分定位(bisect) 终止 标记错误 - 二分定位进行中。当前提交是 '正确' 还是 '错误' ? 标记正确 无法判定 - 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。 + 二分定位进行中。请检出另一个提交并标记其是【正确】还是【错误】 + 二分定位进行中。请标记当前提交为【错误】,或检出另一个提交并标记为【错误】。 + 二分定位进行中。请标记当前的提交是【正确】还是【错误】,然后检出另一个提交。 + 二分定位进行中。当前提交是【正确】还是【错误】? 逐行追溯(blame) 对当前版本的前一版本执行逐行追溯操作 + 忽略空白符变化 选中文件不支持该操作!!! 检出(checkout) ${0}$... - 与当前 ${0}$ 比较 - 与本地工作树比较 + 比较选中的 2 个分支 + 与其他分支或标签比较... + 与当前 HEAD 比较 + 与 ${0}$ 比较 复制分支名 - 创建合并请求 ... - 为上游分支 ${0}$ 创建合并请求 ... + 创建合并请求... + 为上游分支 ${0}$ 创建合并请求... 自定义操作 删除 ${0}$... - 删除选中的 {0} 个分支 + 删除选中的 {0} 个分支... 编辑 ${0}$ 的描述... - 快进(fast-forward)到 ${0}$ + 快进(fast-forward) 到 ${0}$ 拉取(fetch) ${0}$ 至 ${1}$... - GIT工作流 - 完成 ${0}$ - 合并 ${0}$ 到 ${1}$... - 合并 {0} 个分支到当前分支 - 拉回(pull) ${0}$ + GIT工作流 - 完成 ${0}$... + 交互式变基 ${0}$ 到 ${1}$ + 合并(merge) ${0}$ 到 ${1}$... + 合并(merge) {0} 个分支到当前分支... + 拉回(pull) ${0}$... 拉回(pull) ${0}$ 内容至 ${1}$... - 推送(push)${0}$ + 推送(push) ${0}$... 变基(rebase) ${0}$ 至 ${1}$... 重命名 ${0}$... - 重置 ${0}$ 到 ${1}$... + 重置(reset) ${0}$ 到 ${1}$... 切换到 ${0}$ (工作树) - 切换上游分支 ... - 分支比较 + 切换上游分支... 领先 {0} 个提交 领先 {0} 个提交,落后 {1} 个提交 落后 {0} 个提交 @@ -95,6 +105,9 @@ 重置文件到上一版本 重置文件到该版本 生成提交信息 + 解决冲突(内部工具) + 解决冲突(外部工具) + 重置文件到 ${0}$ 切换变更显示模式 文件名+路径列表模式 全路径列表模式 @@ -103,17 +116,18 @@ 子模块 : 远程地址 : 检出(checkout)分支 - 检出(checkout)提交 - 提交 : - 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 未提交更改 : - 丢弃更改 - 贮藏并自动恢复 - 同时更新所有子模块 目标分支 : 您当前游离的HEAD包含未被任何分支及标签引用的提交!是否继续? + 以下子模块需要更新:{0}是否立即更新? 检出分支并快进 上游分支 : + 从所选贮藏检出分支 + 新分支 : + 所选贮藏 : + 检出提交或标签 + 目标 : + 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 挑选提交 提交信息中追加来源信息 提交列表 : @@ -125,6 +139,8 @@ 克隆远程仓库 额外参数 : 其他克隆参数,选填。 + 书签 : + 自定义分组 : 本地仓库名 : 本地仓库目录的名字,选填。 父级目录 : @@ -132,18 +148,23 @@ 远程仓库 : 关闭 提交信息编辑器 - 检出此提交 - 挑选(cherry-pick)此提交 + 分支列表 + 分支 & 标签 + 自定义操作列表 + 文件列表 + 检出此提交... + 挑选(cherry-pick)此提交... 挑选(cherry-pick)... 与当前HEAD比较 与本地工作树比较 作者 + 修改时间 提交信息 提交者 + 提交时间 提交指纹 主题 自定义操作 - 丢弃此提交 交互式变基(rebase -i) 丢弃... 编辑... @@ -151,26 +172,22 @@ 交互式变基 ${0}$ 到 ${1}$ 修改提交信息... 合并至父提交... - 合并(merge)此提交至 ${0}$ 合并(merge)... - 推送(push) ${0}$ 到 ${1}$ - 变基(rebase) ${0}$ 到 ${1} - 重置(reset) ${0}$ 到 ${1} - 回滚此提交 - 编辑提交信息 - 另存为补丁 ... - 合并此提交到上一个提交 - 修复至父提交 + 推送(push) ${0}$ 到 ${1}$... + 变基(rebase) ${0}$ 到 ${1}$... + 重置(reset) ${0}$ 到 ${1}$... + 回滚此提交... + 另存为补丁... 变更对比 个文件发生变更 查找变更... + 折叠详细面板 文件列表 LFS文件 查找文件... 子模块 基本信息 修改者 - 子提交 提交者 查看包含此提交的分支/标签 本提交已被以下分支/标签包含 @@ -185,12 +202,19 @@ 提交指纹 签名者 : 浏览器中查看 + 请输入提交的信息。注意:主题与具体描述中间需要空白行分隔! 主题 + 比较 + 差异文件 + 差异提交 + 左侧独有提交 + 右侧独有提交 + 提示:如果比较的另一方是 HEAD,您可以挑选已选中的提交(使用右键菜单) 仓库配置 提交信息模板 内置变量: - + ${branch_name} 当前分支名 ${files_num} 变更文件数量 ${files} 变更文件路径列表 @@ -202,7 +226,7 @@ 自定义操作 命令行参数 : 内置变量: - + ${REPO} 仓库路径 ${REMOTE} 选中的远程仓库或选中分支所属的远程仓库 ${BRANCH} 选中的分支,对于远程分支不包含远程名 @@ -226,10 +250,12 @@ 电子邮箱 邮箱地址 GIT配置 + 在自动更新子模块前询问用户 启用定时自动拉取远程更新 分钟 自定义规范化提交类型 默认远程 + 自动更新子模块时启用 '--recursive' 选项 默认合并方式 ISSUE追踪 新增匹配Azure DevOps规则 @@ -262,16 +288,20 @@ 名称 : 选项列表 : 选项之间请使用英文 '|' 作为分隔符 + 输出内容格式化字串: + 可选。用于格式化输出结果。当用户输入为空时忽略该操作。请使用`${VALUE}`代替用户输入。 内置变量 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE} 与 ${TAG} 在这里仍然可用 类型 : + 输出结果带有远程名: 工作区 颜色 名称 启动时恢复打开的仓库 确认继续 提交未包含变更文件!是否继续(--allow-empty)? - 自动暂存并提交 - 提交未包含变更文件!是否继续(--allow-empty)或是自动暂存所有变更并提交? + 暂存所有变更并提交 + 仅暂存所选变更并提交 + 提交未包含变更文件!是否继续(--allow-empty)或是自动暂存变更并提交? 系统提示 程序需要重新启动,以便修改生效! 规范化提交信息生成 @@ -283,19 +313,18 @@ 类型: 复制 复制全部文本 + 复制为补丁 复制完整路径 复制路径 - 新建分支 ... + 新建分支... 新分支基于 : 完成后切换到新分支 未提交更改 : - 丢弃更改 - 贮藏并自动恢复 新分支名 : 填写分支名称。 创建本地分支 允许重置已存在的分支 - 新建标签 ... + 新建标签... 标签位于 : 使用GPG签名 标签描述 : @@ -309,13 +338,17 @@ 轻量标签 按住Ctrl键点击将以默认参数运行 剪切 + 丢弃更改 + 不做处理 + 贮藏并自动恢复 取消初始化子模块 强制取消,即使包含本地变更 子模块 : 删除分支确认 + 是否同时删除以下远程分支? 分支名 : + 强制删除,即使包含未合并的提交 您正在删除远程上的分支,请务必小心!!! - 同时删除远程分支 ${0}$ 删除多个分支 您正在尝试一次性删除多个分支,请务必仔细检查后再执行操作! 删除多个标签 @@ -335,7 +368,7 @@ 标签名 : 同时删除远程仓库中的此标签 二进制文件 - 文件权限已变化 + 空文件 首个差异 忽略空白符号变化 混合对比 @@ -355,6 +388,7 @@ 子模块 删除 新增 + + {0} 项未提交变更 交换比对双方 语法高亮 自动换行 @@ -371,12 +405,10 @@ 所有本仓库未提交的修改。 变更 : 包括所有已忽略的文件 + 包括已修改或删除的文件 包括未跟踪的文件 总计{0}项选中更改 本操作不支持回退,请确认后继续!!! - 丢弃提交 - 提交 : - 丢弃后 HEAD : 编辑分支描述 目标 : 书签 : @@ -409,15 +441,25 @@ 文件历史 文件变更 文件内容 + 文件类型变更: + 删除: + 目录 + 可执行文件 + 新增文件类型: + 普通文件 + 子模块 + 符号链接 + 未知 GIT工作流 开发分支 : 特性分支 : 特性分支名前缀 : + 完成 ${0}$ 结束特性分支 结束修复分支 结束版本分支 目标分支 : - 完成后自动推送 + 合并前执行变基操作 压缩变更为单一提交后合并分支 修复分支 : 修复分支名前缀 : @@ -426,10 +468,12 @@ 发布分支 : 版本分支 : 版本分支名前缀 : + 开始于 : 开始特性分支... 开始特性分支 开始修复分支... 开始修复分支 + 分支名 : 输入分支名 开始版本分支... 开始版本分支 @@ -440,7 +484,7 @@ 规则 : 添加LFS追踪文件规则 拉取LFS对象 (fetch) - 执行`git lfs prune`命令,下载远程LFS对象,但不会更新工作副本。 + 执行`git lfs fetch`命令,下载远程LFS对象,但不会更新工作副本。 拉取LFS对象 启用Git LFS支持 显示LFS对象锁 @@ -463,16 +507,26 @@ 远程 : 跟踪名为'{0}'的文件 跟踪所有 *{0} 文件 + 选择前往的提交 历史记录 作者 修改时间 + 提交时间 路线图与主题 提交指纹 - 提交时间 + 高亮分支 + 全部 + 仅当前分支 + 当前分支和选中的提交 + 仅选中的提交 已选中 {0} 项提交 + 显示列 可以按住 Ctrl 或 Shift 键选择多个提交 可以按住 ⌘ 或 ⇧ 键选择多个提交 小提示: + 以独立窗口中打开 + 提交详情 + 比较 快捷键参考 全局快捷键 克隆远程仓库 @@ -480,33 +534,35 @@ 切换到下一个页面 切换到上一个页面 新建页面 + 打开本地仓库 打开偏好设置面板 显示工作区下拉菜单 切换显示页面 + 缩放内容 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 自动暂存全部变更并提交 + 新建分支 拉取 (fetch) 远程变更 切换左边栏为分支/标签等显示模式(默认) + 前往选中提交的子提交 + 前往选中提交的父提交 打开快捷命令面板 切换左边栏为提交搜索模式 拉回 (pull) 远程变更 推送本地变更到远程 重新加载仓库状态 + 展开/折叠历史提交中的详情面板 显示本地更改 显示历史记录 显示贮藏列表 - 文本编辑器 - 关闭搜索 - 定位到下一个匹配搜索的位置 - 定位到上一个匹配搜索的位置 - 使用外部比对工具查看 - 打开搜索 丢弃 暂存 移出暂存区 初始化新仓库 + 是否在该路径下执行 `git init` 命令(初始化仓库)? + 打开本地仓库失败,原因: 路径 : 挑选(Cherry-Pick)操作进行中。 正在处理提交 @@ -518,6 +574,7 @@ 正在回滚提交 交互式变基 自动贮藏并恢复本地变更 + 绕过 pre-rebase 钩子 起始提交 : 拖拽以便对提交重新排序 目标分支 : @@ -526,6 +583,7 @@ 命令列表 出错了 系统提示 + 检测到新版本 打开其他仓库 页面列表 工作区列表 @@ -534,6 +592,26 @@ 目标分支 : 合并方式 : 合并目标 : + 检测合并冲突中... + 合并操作不会产生冲突 + 检测合并冲突时出现未知错误 + 合并操作将存在冲突文件 + 先应用 MINE 后 THEIRS + 先应用 THEIRS 后 MINE + 应用全部 + 所有冲突已解决 + {0} 个冲突未解决 + MINE + 下一个冲突 + 上一个冲突 + 合并结果 + 保存并暂存 + THEIRS + 合并冲突 + 放弃所有更改? + 仅应用 MINE + 仅应用 THEIRS + 撤销更改 合并(多目标) 提交变化 合并策略 : @@ -544,19 +622,24 @@ 调整仓库分组 请选择目标分组: 名称 : + 不用了 GIT尚未配置。请打开【偏好设置】配置GIT路径。 打开 系统默认编辑器 浏览应用数据目录 打开文件 使用外部对比工具查看 + 打开本地仓库 + 书签 : + 分组 : + 仓库位置 : 选填。 新建空白页 - 设置书签 关闭标签页 关闭其他标签页 关闭右侧标签页 复制仓库路径 + 编辑 移至工作区 刷新 新标签页 @@ -573,14 +656,13 @@ 昨天 偏好设置 AI - Analyze Diff Prompt + 附加提示词 (请使用 `-` 列出您的要求) API密钥 - Generate Subject Prompt 模型 + 自动拉取可用模型列表 配置名称 从环境变量(填写环境变量名)中读取API密钥 服务地址 - 启用流式输出 外观配置 缺省字体 编辑器制表符宽度 @@ -607,26 +689,28 @@ 在变更列表树中启用紧凑文件夹模式 显示语言 最大历史提交数 - 在提交路线图中显示修改时间而非提交时间 默认显示【本地更改】页 在提交详情页默认打开【变更对比】标签页 - 在提交详情页中显示子提交列表 + 在提交路线图中显示相对时间 在提交路线图中显示标签 SUBJECT字数检测 + 24小时制 + 在提交路线图中使用精简分支名 生成GitHub风格的默认头像 GIT配置 自动换行转换 默认克隆路径 邮箱 默认GIT用户邮箱 - 拉取更新时启用修剪(--prune) - 对比文件时,默认忽略换行符变更 (--ignore-cr-at-eol) + 拉取更新时启用修剪 + 对比文件时,默认忽略换行符变更 本软件要求GIT最低版本为2.25.1 安装路径 启用HTTP SSL验证 使用 git-credential-libsecret 替代 git-credential-manager 用户名 默认GIT用户名 + 迁出或合并分支时,默认贮藏并自动恢复本地更改 Git 版本 GPG签名 启用提交签名 @@ -650,9 +734,6 @@ 拉取分支 : 本地分支 : 未提交更改 : - 丢弃更改 - 贮藏并自动恢复 - 同时更新所有子模块 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -677,7 +758,12 @@ 退出 变基(rebase)操作 自动贮藏并恢复本地变更 + 绕过 pre-rebase 钩子 目标提交 : + 检测变基冲突中... + 未检测到文件冲突 + 检测变基冲突时出现未知错误 + 检测到冲突文件 添加远程仓库 编辑远程仓库 远程名 : @@ -686,8 +772,9 @@ 远程仓库的地址 复制远程地址 自定义操作 - 删除 ... - 编辑 ... + 删除... + 编辑... + 启用自动拉取 拉取(fetch)更新 在浏览器中打开 清理远程已删除分支 @@ -716,9 +803,12 @@ 在文件浏览器中打开 快速查找分支/标签/子模块 设置在列表中的可见性 + 折叠过滤项列表 不指定 在提交列表中隐藏 + 展开过滤项列表 使用其对提交列表过滤 + 个过滤项已被启用 布局方式 水平排布 竖直排布 @@ -730,14 +820,14 @@ 定位HEAD 新建分支 清空通知列表 - 仅高亮显示当前分支 + 本仓库(文件夹) 在 {0} 中打开 使用外部工具打开 远程列表 添加远程 + 解决冲突 查找提交 作者 - 提交者 变更内容 提交信息 路径 @@ -760,7 +850,6 @@ 按名称 排序 在终端中打开 - 在提交列表中使用相对时间 查看命令日志 访问远程仓库 '{0}' 工作树列表 @@ -778,7 +867,6 @@ 回滚操作确认 目标提交 : 回滚后提交更改 - 编辑提交信息 执行操作中,请耐心等待... 保 存 另存为... @@ -788,9 +876,11 @@ 扫描其他自定义路径 检测更新... 检测到软件有版本更新: + 当前版本 : 获取最新版本信息失败! 下 载 忽略此版本 + 新版发布时间 : 软件更新 当前已是最新版本。 修改子模块追踪分支 @@ -804,9 +894,6 @@ 上游分支 : 复制提交指纹 跳转到提交 - 合并修改至父提交 - 修复至父提交 - 父提交: SSH密钥 : SSH密钥文件 开 始 @@ -819,6 +906,8 @@ 选中文件的所有变更均会被贮藏! 贮藏本地变更 应用(apply) + 应用(apply)选中变更 + 检出新分支 复制描述信息 删除(drop) 另存为补丁... @@ -855,9 +944,15 @@ 未解决冲突 更新 仓库 + 子模块变更详情 + 查看变更详情 确 定 创建者 创建时间 + 检出标签... + 比较 2 个标签 + 与其他分支或标签比较... + 与当前 HEAD 比较 标签信息 标签名 创建者 @@ -870,8 +965,8 @@ 更新子模块 更新所有子模块 如未初始化子模块,先初始化 - 递归更新子孙模块 子模块 : + 递归更新子孙模块 更新到子模块自己的远程踪分支 仓库地址 : 日志列表 @@ -898,6 +993,7 @@ 忽略同目录下所有 *{0} 文件 忽略该目录下的新文件 忽略本文件 + 忽略同目录下所有新文件 修补 现在您已可将其加入暂存区中 清空历史提交信息 @@ -911,11 +1007,13 @@ 您正在向一个游离的 HEAD 提交变更,是否继续提交? 当前有 {0} 个文件在暂存区中,但仅显示了 {1} 个文件({2} 个文件被过滤掉了),是否继续提交? 检测到冲突 - 打开合并工具 + 解决冲突 + 使用外部工具解决冲突 打开合并工具解决冲突 文件冲突已解决 使用 MINE 使用 THEIRS + 查找变更... 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 @@ -933,9 +1031,13 @@ 工作区: 配置工作区... 本地工作树 + 連結分支 复制工作树路径 + 最新提交 锁定工作树 打开工作树 + 位置 移除工作树 解除工作树锁定 + 好的 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 9178f6fef..b07c82996 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -5,14 +5,15 @@ 關於 關於 SourceGit + 發行日期: {0} 版本說明 開源免費的 Git 客戶端 新增忽略檔案 比對模式: 儲存路徑: - 新增工作區 - 工作區路徑: - 填寫該工作區的路徑。支援相對路徑。 + 新增工作樹 + 工作樹路徑: + 填寫該工作樹的路徑。支援相對路徑。 分支名稱: 選填。預設使用目標資料夾名稱。 追蹤分支 @@ -21,15 +22,19 @@ 建立新分支 已有分支 AI 助理 + 模型 重新產生 使用 AI 產生提交訊息 - 套用為提交訊息 + 套用 隱藏 SourceGit - 顯示所有 - 套用修補檔 (apply patch) - 修補檔: + 隱藏其他 + 顯示全部 + 嘗試三向合併 選擇修補檔 忽略空白符號 + 修補檔來源: + 檔案 + 剪貼簿 套用修補檔 空白字元處理: 套用擱置變更 @@ -51,50 +56,58 @@ 二分搜尋 (bisect) 中止 標記為錯誤 - 二分搜尋進行中。目前的提交是「良好」是「錯誤」? 標記為良好 無法確認 - 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 + 二分搜尋進行中。請簽出另一個提交,然後標記該提交為「良好」或「錯誤」。 + 二分搜尋進行中。請標記目前的提交為「錯誤」,或簽出另一個提交並標記為「錯誤」。 + 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 + 二分搜尋進行中。目前的提交是「良好」還是「錯誤」? 逐行溯源 (blame) 對上一個版本執行逐行溯源 + 忽略空白符號變化 所選擇的檔案不支援該操作! 簽出 (checkout) ${0}$... - 與目前 ${0}$ 比較 - 與本機工作區比較 + 比較所選的 2 個分支 + 與其他分支或標籤比較... + 與目前 HEAD 比較 + 與 ${0}$ 比較 複製分支名稱 建立拉取請求... 為上游分支 ${0}$ 建立拉取請求... 自訂動作 刪除 ${0}$... - 刪除所選的 {0} 個分支 + 刪除所選的 {0} 個分支... 編輯 ${0}$ 的描述... 快轉 (fast-forward) 到 ${0}$ 提取 (fetch) ${0}$ 到 ${1}$... - Git 工作流 - 完成 ${0}$ + Git 工作流 - 完成 ${0}$... + 互動式重定基底 ${0}$ 至 ${1}$ 合併 ${0}$ 到 ${1}$... - 合併 {0} 個分支到目前分支 - 拉取 (pull) ${0}$ + 合併 {0} 個分支到目前分支... + 拉取 (pull) ${0}$... 拉取 (pull) ${0}$ 內容至 ${1}$... - 推送 (push) ${0}$ + 推送 (push) ${0}$... 重定基底 (rebase) ${0}$ 分支至 ${1}$... 重新命名 ${0}$... 重設 ${0}$ 至 ${1}$... - 切換到 ${0}$ (工作區) + 切換到 ${0}$ (工作樹) 切換上游分支... - 分支比較 領先 {0} 次提交 - 領先 {0} 次提交,落後 {0} 次提交 + 領先 {0} 次提交,落後 {1} 次提交 落後 {0} 次提交 無效 遠端 狀態 上游分支 遠端網址 - 工作區 + 工作樹 取 消 重設檔案到上一版本 重設檔案為此版本 產生提交訊息 + 解決衝突 (內建工具) + 解決衝突 (外部工具) + 重設檔案到 ${0}$ 切換變更顯示模式 檔案名稱 + 路徑列表模式 全路徑列表模式 @@ -103,28 +116,31 @@ 子模組: 遠端網址: 簽出 (checkout) 分支 - 簽出 (checkout) 提交 - 提交: - 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 未提交變更: - 捨棄變更 - 擱置變更並自動復原 - 同時更新所有子模組 目標分支: - 您目前的分離的 HEAD 包含與任何分支/標籤無關的提交! 您要繼續嗎? + 目前的 HEAD 包含未連結至任何分支或標籤的提交! 您是否要繼續? + 以下子模組需要更新: {0},您要立即更新嗎? 簽出分支並快轉 上游分支: + 從所選擱置簽出分支 + 新分支: + 所選擱置: + 簽出提交或標籤 + 目標: + 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 揀選提交 提交資訊中追加來源資訊 提交列表: 提交變更 對比的父提交: - 通常您不能對一個合併進行揀選 (cherry-pick),因為您不知道合併的哪一邊應該被視為主線。這個選項指定了作為主線的父提交,允許揀選相對於該提交的修改。 + 通常您無法揀選 (cherry-pick) 合併提交,因為無法判斷哪個父提交應視為主線。此選項可指定作為主線的父提交。 捨棄擱置變更確認 您正在捨棄所有的擱置變更,一經操作便無法復原,是否繼續? 複製 (clone) 遠端存放庫 額外參數: 其他複製參數,選填。 + 書籤: + 群組: 本機存放庫名稱: 本機存放庫目錄的名稱,選填。 上層目錄: @@ -132,18 +148,23 @@ 遠端存放庫: 關閉 提交訊息編輯器 - 簽出 (checkout) 此提交 - 揀選 (cherry-pick) 此提交 + 分支列表 + 分支 & 標籤 + 自訂動作 + 檔案列表 + 簽出 (checkout) 此提交... + 揀選 (cherry-pick) 此提交... 揀選 (cherry-pick)... 與目前 HEAD 比較 - 與本機工作區比較 + 與本機工作樹比較 作者 + 修改時間 提交訊息 提交者 + 提交時間 提交編號 標題 自訂動作 - 捨棄此提交 互動式重定基底 (rebase -i) 捨棄... 編輯... @@ -151,26 +172,22 @@ 互動式重定基底 ${0}$ 至 ${1}$ 編輯提交訊息... 合併至父提交... - 合併 (merge) 此提交到 ${0}$ 合併 (merge)... - 推送(push) ${0}$ 至 ${1}$ - 重定基底 (rebase) ${0}$ 至 ${1}$ - 重設 (reset) ${0}$ 至 ${1}$ - 復原此提交 - 編輯提交訊息 + 推送 (push) ${0}$ 至 ${1}$... + 重定基底 (rebase) ${0}$ 至 ${1}$... + 重設 (reset) ${0}$ 至 ${1}$... + 復原此提交... 另存為修補檔 (patch)... - 合併此提交到上一個提交 - 修正至父提交 變更對比 個檔案已變更 搜尋變更... + 收合詳細面板 檔案列表 LFS 檔案 搜尋檔案... 子模組 基本資訊 作者 - 後續提交 提交者 檢視包含此提交的分支或標籤 本提交包含於以下分支或標籤 @@ -185,8 +202,15 @@ 提交編號 簽署人: 在瀏覽器中檢視 + 請輸入提交訊息,標題與詳細描述之間請使用單行空白區隔。 標題 + 比較 + 差異檔案 + 差異提交 + 左側獨有提交 + 右側獨有提交 + 提示: 如果比較的另一方是 HEAD,您可以揀選 (cherry-pick) 已選擇的提交 (使用滑鼠右鍵) 存放庫設定 提交訊息範本 內建參數: @@ -205,8 +229,8 @@ ${REPO} 存放庫路徑 ${REMOTE} 所選的遠端存放庫或所選分支的遠端 - ${BRANCH} 所選的分支。對於遠端分支,不包含遠端名稱 - ${BRANCH_FRIENDLY_NAME} 所選的分支。對於遠端分支,不包含遠端名稱 + ${BRANCH} 所選的分支。遠端分支不包含遠端名稱 + ${BRANCH_FRIENDLY_NAME} 所選的分支。遠端分支會包含遠端名稱 ${SHA} 所選的提交編號 ${TAG} 所選的標籤 ${FILE} 所選的檔案,相對於存放庫根目錄的路徑 @@ -226,10 +250,12 @@ 電子郵件 電子郵件地址 Git 設定 + 在自動更新子模組之前先詢問 啟用定時自動提取 (fetch) 遠端更新 分鐘 自訂約定式提交類型 預設遠端存放庫 + 自動更新子模組時啟用「--recursive」選項 預設合併模式 Issue 追蹤 新增符合 Azure DevOps 規則 @@ -255,23 +281,27 @@ 用於本存放庫的使用者名稱 編輯自訂動作輸入控制項 啟用時的指令參數: - 勾選 CheckBox 後,此值將用於指令參數中 + 勾選核取方塊後,此值將用於指令參數中 描述: 預設值: 目標路徑是否為資料夾: 名稱: 選項列表: 請使用英文「|」符號分隔選項 + 字串格式化: + 選填。用於格式化輸出字串。若輸入為空則會忽略此設定。請使用 `${VALUE}` 來表示輸入字串。 內建變數 ${REPO}、${REMOTE}、${BRANCH}、${BRANCH_FRIENDLY_NAME}、${SHA}、${FILE} 及 ${TAG} 在此處仍可使用 類型: + 輸出包含遠端的名稱: 工作區 顏色 名稱 啟動時還原上次開啟的存放庫 確認繼續 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)? - 自動暫存並提交 - 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交? + 暫存全部變更並提交 + 僅暫存所選變更並提交 + 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或自動暫存變更並提交? 系統提示 您需要重新啟動此應用程式才能套用變更! 產生約定式提交訊息 @@ -283,14 +313,13 @@ 類型: 複製 複製全部內容 + 複製為修補檔 複製完整路徑 複製路徑 新增分支... 新分支基於: 完成後切換到新分支 未提交變更: - 捨棄變更 - 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 建立本機分支 @@ -309,13 +338,17 @@ 輕量標籤 按住 Ctrl 鍵將直接以預設參數執行 剪下 + 捨棄變更 + 不做處理 + 擱置變更並自動復原 取消初始化子模組 強制取消,即使包含本機變更 - 子模組: + 子模組: 刪除分支確認 + 您是否也想刪除以下遠端分支? 分支名稱: + 強制刪除,即使包含未合併的提交 您正在刪除遠端上的分支,請務必小心! - 同時刪除遠端分支 ${0}$ 刪除多個分支 您正在嘗試一次性刪除多個分支,請務必仔細檢查後再刪除! 刪除多個標籤 @@ -335,7 +368,7 @@ 標籤名稱: 同時刪除遠端存放庫中的此標籤 二進位檔案 - 檔案權限已變更 + 空檔案 第一個差異 忽略空白符號變化 混合對比 @@ -355,6 +388,7 @@ 子模組 已刪除 新增 + + {0} 項未提交變更 交換比對雙方 語法上色 自動換行 @@ -363,7 +397,7 @@ 減少可見的行數 增加可見的行數 請選擇需要對比的檔案 - 目錄内容變更歷史 + 目錄內容變更歷史 未提交的本機變更 目前分支 HEAD 與上游不相符 已更新至最新 @@ -371,12 +405,10 @@ 所有本機未提交的變更。 變更: 包括所有已忽略的檔案 + 包括已變更或刪除的檔案 包含未追蹤檔案 將捨棄總計 {0} 項已選取的變更 您無法復原此操作,請確認後再繼續! - 捨棄提交 - 提交: - 捨棄後新的 HEAD: 編輯分支的描述 目標: 書籤: @@ -408,16 +440,26 @@ 使用對方版本 (theirs) 檔案歷史 檔案變更 - 檔案内容 + 檔案內容 + 檔案類型變更: + 刪除檔案類型: + 目錄 + 可執行檔案 + 新增檔案類型: + 一般檔案 + 子模組 + 符號連結 + 未知 Git 工作流 開發分支: 功能分支: 功能分支前置詞: + 完成 ${0}$ 完成功能分支 完成修復分支 完成發行分支 目標分支: - 完成後自動推送 + 合併前重定基底 壓縮為單一提交後合併 修復分支: 修復分支前置詞: @@ -426,10 +468,12 @@ 發行分支: 版本分支: 發行分支前置詞: + 開始於: 開始功能分支... 開始功能分支 開始修復分支... 開始修復分支 + 分支名稱: 輸入分支名稱 開始發行分支... 開始發行分支 @@ -463,16 +507,26 @@ 遠端存放庫: 追蹤名稱為「{0}」的檔案 追蹤所有 *{0} 檔案 + 選取要前往的提交 歷史記錄 作者 修改時間 + 提交時間 路線圖與訊息標題 提交編號 - 提交時間 + 上色分支 + 全部 + 僅目前分支 + 目前分支和選取的提交 + 僅選取的提交 已選取 {0} 項提交 + 顯示欄位 可以按住 Ctrl 或 Shift 鍵選擇多個提交 可以按住 ⌘ 或 ⇧ 鍵選擇多個提交 小提示: + 在新視窗中開啟 + 提交詳細資訊 + 比較 快速鍵參考 全域快速鍵 複製 (clone) 遠端存放庫 @@ -480,33 +534,35 @@ 切換到下一個頁面 切換到上一個頁面 新增頁面 + 開啟本機存放庫 開啟偏好設定面板 顯示工作區的下拉式選單 切換目前頁面 + 放大/縮小內容 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 自動暫存全部變更並提交 + 新增分支 提取 (fetch) 遠端的變更 切換左邊欄為分支/標籤等顯示模式 (預設) + 前往所選提交的子提交 + 前往所選提交的父提交 開啟命令面板 切換左邊欄為歷史搜尋模式 拉取 (pull) 遠端的變更 推送 (push) 本機變更到遠端存放庫 強制重新載入存放庫 + 在歷史頁面中展開/收合詳細面板 顯示本機變更 顯示歷史記錄 顯示擱置變更列表 - 文字編輯器快速鍵 - 關閉搜尋面板 - 前往下一個搜尋相符的位置 - 前往上一個搜尋相符的位置 - 使用外部比對工具檢視 - 開啟搜尋面板 捨棄 暫存 取消暫存 初始化存放庫 + 您是否要在該路徑執行 `git init` 以初始化存放庫? + 無法在指定路徑開啟本機存放庫。原因: 路徑: 揀選 (cherry-pick) 操作進行中。 正在處理提交 @@ -518,6 +574,7 @@ 正在復原提交 互動式重定基底 自動擱置變更並復原本機變更 + 繞過 pre-rebase hook 起始提交: 拖曳以重新排序提交 目標分支: @@ -526,6 +583,7 @@ 命令列表 發生錯誤 系統提示 + 有新版本可用 開啟存放庫 頁面列表 工作區列表 @@ -534,6 +592,26 @@ 目標分支: 合併方式: 合併來源: + 正在偵測合併衝突... + 合併操作不會產生衝突 + 偵測合併衝突時發生未知錯誤 + 合併操作將產生檔案衝突 + 先套用我方版本 (ours),再套用對方版本 (theirs) + 先套用對方版本 (theirs),再套用我方版本 (ours) + 套用兩者 + 所有衝突已經解決 + {0} 個衝突尚未解決 + 我方版本 (ours) + 下一個衝突 + 上一個衝突 + 合併結果 + 儲存並暫存 + 對方版本 (theirs) + 解決衝突 + 捨棄未儲存的變更? + 僅套用我方版本 (ours) + 僅套用對方版本 (theirs) + 復原變更 合併 (多個來源) 提交變更 合併策略: @@ -544,19 +622,24 @@ 調整存放庫分組 請選擇目標分組: 名稱: + 尚未設定 Git。請開啟 [偏好設定] 以設定 Git 路徑。 開啟 系統預設編輯器 瀏覽程式資料目錄 開啟檔案 使用外部比對工具檢視 + 開啟本機存放庫 + 書籤: + 群組: + 存放庫位置: 選填。 新增分頁 - 設定書籤 關閉分頁 關閉其他分頁 關閉右側分頁 複製存放庫路徑 + 編輯 移至工作區 重新整理 新分頁 @@ -573,14 +656,13 @@ 昨天 偏好設定 AI - 分析變更差異提示詞 + 附加提示詞 (請使用 '-' 列出您的要求) API 金鑰 - 產生提交訊息提示詞 模型 + 自動取得可用的模型列表 名稱 從環境變數中 (輸入環境變數名稱) 讀取 API 金鑰 伺服器 - 啟用串流輸出 外觀設定 預設字型 編輯器 Tab 寬度 @@ -595,9 +677,9 @@ 使用系統原生預設視窗樣式 對比/合併工具 對比命令參數 - 可用參數:$LOCAL, $REMOTE + 可用參數: $LOCAL, $REMOTE 合併命令參數 - 可用參數:$BASE, $LOCAL, $REMOTE, $MERGED + 可用參數: $BASE, $LOCAL, $REMOTE, $MERGED 安裝路徑 填寫可執行檔案所在路徑 工具 @@ -607,26 +689,28 @@ 在樹狀變更目錄中啟用密集資料夾模式 顯示語言 最大歷史提交數 - 在提交路線圖中顯示修改時間而非提交時間 預設顯示 [本機變更] 頁面 在提交詳細資訊頁面預設顯示 [變更對比] - 在提交詳細資訊中顯示後續提交 + 在提交路線圖中顯示相對時間 在路線圖中顯示標籤 提交標題字數偵測 + 24 小時制 + 在提交路線圖中使用簡潔的分支標籤 產生 GitHub 風格的預設頭貼 Git 設定 自動換行轉換 預設複製 (clone) 路徑 電子郵件 預設 Git 使用者電子郵件 - 拉取變更時進行清理 (--prune) - 對比檔案時,預設忽略行末的 CR 變更 (--ignore-cr-at-eol) + 提取後清理遠端已刪除分支 + 對比檔案時,預設忽略行末的 CR 變更 本軟體要求 Git 最低版本為 2.25.1 安裝路徑 啟用 HTTP SSL 驗證 使用 git-credential-libsecret 取代 git-credential-manager 使用者名稱 預設 Git 使用者名稱 + 在簽出或合併分支時,預設擱置變更並自動復原 Git 版本 GPG 簽章 啟用提交簽章 @@ -639,20 +723,17 @@ 第三方工具整合 終端機/Shell 啟動參數 - 請使用「.」標示當前工作目錄 + 請使用「.」表示目前工作目錄 安裝路徑 終端機/Shell 清理遠端已刪除分支 目標: - 清理工作區 - 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作區資訊 + 清理工作樹 + 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作樹資訊 拉取 (pull) 拉取分支: 本機分支: 未提交變更: - 捨棄變更 - 擱置變更並自動復原 - 同時更新所有子模組 遠端: 拉取 (提取並合併) 使用重定基底 (rebase) 合併分支 @@ -677,7 +758,12 @@ 結束 重定基底 (rebase) 操作 自動擱置變更並復原本機變更 + 繞過 pre-rebase hook 目標提交: + 正在偵測重定基底衝突... + 重定基底操作不會產生衝突 + 偵測重定基底衝突時發生未知錯誤 + 重定基底操作將產生檔案衝突 新增遠端存放庫 編輯遠端存放庫 遠端名稱: @@ -688,21 +774,22 @@ 自訂動作 刪除... 編輯... + 啟用定時自動提取 提取 (fetch) 更新 - 在瀏覽器中存取網址 + 在瀏覽器中開啟網址 清理遠端已刪除分支 - 刪除工作區操作確認 + 刪除工作樹操作確認 啟用 [--force] 選項 - 目標工作區: + 目標工作樹: 分支重新命名 新名稱: 新的分支名稱不能與現有分支名稱相同 分支: 中止 - 自動提取遠端變更中... + 正在自動提取遠端變更... 排序 依建立時間 - 依名稱升序 + 依名稱遞增排序 清理本存放庫 (GC) 本操作將執行 `git gc` 指令。 清空篩選規則 @@ -716,9 +803,12 @@ 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 篩選以顯示或隱藏 + 摺疊篩選器 取消指定 在提交列表中隱藏 + 展開篩選器 以其篩選提交列表 + 個篩選規則已啟用 版面配置 橫向顯示 縱向顯示 @@ -730,14 +820,14 @@ 回到 HEAD 新增分支 清除所有通知 - 路線圖中僅對目前分支上色 + 此存放庫 (資料夾) 在 {0} 中開啟 使用外部工具開啟 遠端列表 新增遠端 + 解決衝突 搜尋提交 作者 - 提交者 變更內容 提交訊息 路徑 @@ -747,8 +837,8 @@ 只顯示合併提交的第一父提交 顯示內容 顯示失去參照的提交 - 以樹型結構展示 - 以樹型結構展示 + 以樹狀結構顯示 + 以樹狀結構顯示 跳過此提交 提交統計 子模組列表 @@ -760,11 +850,10 @@ 依名稱 排序 在終端機中開啟 - 在提交列表中使用相對時間 檢視 Git 指令記錄 檢視遠端存放庫 '{0}' - 工作區列表 - 新增工作區 + 工作樹列表 + 新增工作樹 清理 遠端存放庫網址 重設目前分支到指定版本 @@ -778,8 +867,7 @@ 復原操作確認 目標提交: 復原後提交變更 - 編輯提交訊息 - 執行操作中,請耐心等待... + 正在執行操作,請耐心等待... 儲存 另存新檔... 修補檔已成功儲存! @@ -788,9 +876,11 @@ 掃描其他自訂目錄 檢查更新... 軟體有版本更新: + 目前版本: 取得最新版本資訊失敗! 下載 忽略此版本 + 新版本發行日期: 軟體更新 目前已是最新版本。 設定子模組的追蹤分支 @@ -804,9 +894,6 @@ 上游分支: 複製提交編號 前往此提交 - 合併變更入父提交 - 修正至父提交 - 父提交: SSH 金鑰: SSH 金鑰檔案 開 始 @@ -819,6 +906,8 @@ 已選取的檔案中的變更均會被擱置! 擱置本機變更 套用 (apply) + 套用 (apply) 所選變更 + 簽出分支 複製描述訊息 刪除 (drop) 另存為修補檔 (patch)... @@ -846,8 +935,8 @@ 相對存放庫路徑: 本機存放的相對路徑。 刪除 - 更改追蹤分支 - 更改遠端網址 + 變更追蹤分支 + 變更遠端網址 狀態 未提交變更 未初始化 @@ -855,9 +944,15 @@ 未解決的衝突 更新 存放庫 + 子模組變更詳細資訊 + 開啟變更詳細資訊 確 定 建立者 建立時間 + 簽出標籤... + 比較 2 個標籤 + 與其他分支或標籤比較... + 與目前 HEAD 比較 標籤訊息 標籤名稱 建立者 @@ -870,8 +965,8 @@ 更新子模組 更新所有子模組 如果子模組尚未初始化,則將其初始化 - 遞迴更新子孫模組 子模組: + 遞迴更新子模組 更新至子模組的遠端追蹤分支 存放庫網址: 記錄 @@ -887,7 +982,7 @@ 支援拖放以新增目錄與自訂群組。 編輯 調整存放庫分組 - 開啟所有包含存放庫 + 開啟所有存放庫 開啟本機存放庫 開啟終端機 重新掃描預設複製 (clone) 目錄下的存放庫 @@ -898,6 +993,7 @@ 忽略同路徑下所有 *{0} 檔案 忽略本路徑下的新增檔案 忽略本檔案 + 忽略同路徑下所有新增檔案 修補 現在您已可將其加入暫存區中 清除提交訊息歷史 @@ -908,14 +1004,16 @@ 觸發點擊事件 提交 (修改原始提交) 自動暫存全部變更並提交 - 您正在向一个分離狀態的 HEAD 提交變更,您確定要繼續提交嗎? + HEAD 目前處於分離狀態。您確定要繼續提交變更嗎? 您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎? 偵測到衝突 - 使用外部合併工具開啟 + 解決衝突 + 使用外部工具解決衝突 使用外部合併工具開啟 檔案衝突已解決 使用我方版本 (ours) 使用對方版本 (theirs) + 搜尋變更... 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 @@ -932,10 +1030,14 @@ 範本: ${0}$ 工作區: 設定工作區... - 本機工作區 - 複製工作區路徑 - 鎖定工作區 - 開啟工作區 - 移除工作區 - 解除鎖定工作區 + 本機工作樹 + 分支 + 複製工作樹路徑 + 最新提交 + 鎖定工作樹 + 開啟工作樹 + 位置 + 移除工作樹 + 解除鎖定工作樹 + diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 651bc9505..27878b5cc 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -1,11 +1,9 @@ @@ -13,24 +11,13 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - + + + + + + - - - - - - + - - + diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index 84b89f43b..414316380 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -6,17 +6,21 @@ #FF999999 #FFCFDEEA #FFF0F5F9 - #FFF8F8F8 + #FFF4F8FB + White #FFFAFAFA #FFB0CEE8 #FF1F1F1F #FF836C2E - #FFFFFFFF + #FFFFFFFF + #400078D7 + #40FF8C00 #FFCFCFCF #FF898989 #FFCFCFCF #FFF8F8F8 White + #FF898989 #FF1F1F1F #FF6F6F6F #10000000 @@ -24,8 +28,11 @@ #80FF9797 #A7E1A7 #F19B9D + DarkCyan #0000EE #FFE4E4E4 + Black + #FFF0F5F9 @@ -34,25 +41,32 @@ #FF1F1F1F #FF2F2F2F #FF2B2B2B + #FF393939 #FF1C1C1C #FF8F8F8F - #FFDDDDDD + #FFE0E0E0 #FFFAFAD2 - #FF252525 + #FF252525 + #400078D7 + #40FF8C00 #FF181818 #FF7C7C7C #FF404040 #FF303030 #FF333333 - #FFDDDDDD - #40F1F1F1 + #FF4F4F4F + #FFDFDFDF + #FF9F9F9F #3C000000 #C03A5C3F #C0633F3E #A0308D3C #A09F4247 + DarkCyan #4DAAFC #FF383838 + #FFF0F0F0 + #FF272727 @@ -61,16 +75,20 @@ + - + + + + @@ -80,9 +98,12 @@ + + + fonts:Inter#Inter - fonts:SourceGit#JetBrains Mono + fonts:SourceGit#JetBrains Mono NL diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 9cc5c3f76..fc8fe02d1 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -1,4 +1,4 @@ - + WinExe net10.0 @@ -7,6 +7,7 @@ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)\\..\\VERSION")) true true + false SourceGit OpenSource GIT client @@ -32,6 +33,10 @@ $(DefineConstants);DISABLE_UPDATE_DETECTION + + + + @@ -43,26 +48,36 @@ - - - - - - - - - + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index 920fafcf5..e9e4692fe 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -1,16 +1,26 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; - using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { public class AIAssistant : ObservableObject { + public List AvailableModels + { + get => _service.AvailableModels; + } + + public string CurrentModel + { + get => _service.Model; + set => _service.Model = value; + } + public bool IsGenerating { get => _isGenerating; @@ -23,28 +33,90 @@ public string Text private set => SetProperty(ref _text, value); } - public AIAssistant(Repository repo, Models.OpenAIService service, List changes, Action onApply) + public string Response + { + get => _response; + private set => SetProperty(ref _response, value); + } + + public AIAssistant(Repository repo, AI.Service service, List changes) { _repo = repo; _service = service; - _changes = changes; - _onApply = onApply; _cancel = new CancellationTokenSource(); - Gen(); + var builder = new StringBuilder(); + foreach (var c in changes) + SerializeChange(c, builder); + _changeList = builder.ToString(); + + if (changes.Count > 0 && changes[0].DataForAmend is { } amend) + _amendParent = amend.ParentSHA; } - public void Regen() + public async Task GenAsync() { if (_cancel is { IsCancellationRequested: false }) _cancel.Cancel(); + _cancel = new CancellationTokenSource(); + + var agent = new AI.Agent(_service); + var builder = new StringBuilder(); + builder.AppendLine("Asking AI to generate commit message...").AppendLine(); - Gen(); + var responseBuilder = new StringBuilder(); + var foundResponse = false; + var currentBranchName = _repo.CurrentBranch?.Name ?? "main"; + + Text = builder.ToString(); + Response = string.Empty; + IsGenerating = true; + + try + { + await agent.GenerateCommitMessageAsync(_repo.FullPath, currentBranchName, _changeList, _amendParent, message => + { + builder.AppendLine(message); + + if (foundResponse) + { + if (message.Equals("# Token Usage", StringComparison.Ordinal)) + foundResponse = false; + else + responseBuilder.AppendLine(message); + } + else if (message.Equals("# Assistant", StringComparison.Ordinal)) + { + foundResponse = true; + } + + Dispatcher.UIThread.Post(() => Text = builder.ToString()); + }, _cancel.Token); + + Response = responseBuilder.ToString().Trim(); + } + catch (OperationCanceledException) + { + // Do nothing and leave `IsGenerating` to current (may already changed), so that the UI can update accordingly. + return; + } + catch (Exception e) + { + builder + .AppendLine() + .AppendLine("[ERROR]") + .Append(e.Message); + + Text = builder.ToString(); + Response = string.Empty; + } + + IsGenerating = false; } - public void Apply() + public void Use(string text) { - _onApply?.Invoke(Text); + _repo.SetCommitMessage(text); } public void Cancel() @@ -52,29 +124,34 @@ public void Cancel() _cancel?.Cancel(); } - private void Gen() + private void SerializeChange(Models.Change c, StringBuilder builder) { - Text = string.Empty; - IsGenerating = true; - - _cancel = new CancellationTokenSource(); - Task.Run(async () => + var status = c.Index switch { - await new Commands.GenerateCommitMessage(_service, _repo.FullPath, _changes, _cancel.Token, message => - { - Dispatcher.UIThread.Post(() => Text = message); - }).ExecAsync().ConfigureAwait(false); + Models.ChangeState.Added => "A", + Models.ChangeState.Modified => "M", + Models.ChangeState.Deleted => "D", + Models.ChangeState.TypeChanged => "T", + Models.ChangeState.Renamed => "R", + Models.ChangeState.Copied => "C", + _ => " ", + }; + + builder.Append(status).Append('\t'); - Dispatcher.UIThread.Post(() => IsGenerating = false); - }, _cancel.Token); + if (c.Index == Models.ChangeState.Renamed || c.Index == Models.ChangeState.Copied) + builder.Append(c.OriginalPath).Append(" -> ").Append(c.Path).AppendLine(); + else + builder.Append(c.Path).AppendLine(); } private readonly Repository _repo = null; - private Models.OpenAIService _service = null; - private List _changes = null; - private Action _onApply = null; + private readonly AI.Service _service = null; + private readonly string _changeList = null; + private readonly string _amendParent = null; private CancellationTokenSource _cancel = null; private bool _isGenerating = false; private string _text = string.Empty; + private string _response = string.Empty; } } diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index fb5f0264b..f5182ef9f 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -44,6 +44,12 @@ public string SSHKey set => SetProperty(ref _sshkey, value, true); } + public bool FetchWithoutTags + { + get; + set; + } = false; + public AddRemote(Repository repo) { _repo = repo; @@ -105,7 +111,7 @@ public override async Task Sure() .Use(log) .SetAsync($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); - await new Commands.Fetch(_repo.FullPath, _name, false, false) + await new Commands.Fetch(_repo.FullPath, _name, FetchWithoutTags, false) .Use(log) .RunAsync(); } diff --git a/src/ViewModels/AddToIgnore.cs b/src/ViewModels/AddToIgnore.cs index a502c1725..c88cf1e7f 100644 --- a/src/ViewModels/AddToIgnore.cs +++ b/src/ViewModels/AddToIgnore.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -6,6 +7,11 @@ namespace SourceGit.ViewModels { public class AddToIgnore : Popup { + public List StorageFiles + { + get; + } + [Required(ErrorMessage = "Ignore pattern is required!")] public string Pattern { @@ -14,17 +20,23 @@ public string Pattern } [Required(ErrorMessage = "Storage file is required!!!")] - public Models.GitIgnoreFile StorageFile + public Models.GitIgnoreFile SelectedStorageFile { - get; - set; + get => _selectedStorageFile; + set + { + if (SetProperty(ref _selectedStorageFile, value, true)) + Pattern = value.Pattern; + } } public AddToIgnore(Repository repo, string pattern) { _repo = repo; _pattern = pattern; - StorageFile = Models.GitIgnoreFile.Supported[0]; + + StorageFiles = Models.GitIgnoreFile.GetSupported(repo.FullPath, repo.GitDir, pattern); + SelectedStorageFile = StorageFiles[0]; } public override async Task Sure() @@ -32,7 +44,7 @@ public override async Task Sure() using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding Ignored File(s) ..."; - var file = StorageFile.GetFullPath(_repo.FullPath, _repo.GitDir); + var file = _selectedStorageFile.FullPath; if (!File.Exists(file)) { await File.WriteAllLinesAsync(file!, [_pattern]); @@ -52,5 +64,6 @@ public override async Task Sure() private readonly Repository _repo; private string _pattern; + private Models.GitIgnoreFile _selectedStorageFile; } } diff --git a/src/ViewModels/AddWorktree.cs b/src/ViewModels/AddWorktree.cs index 7679b4509..8f8aea2c4 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; @@ -23,61 +24,71 @@ public bool CreateNewBranch if (SetProperty(ref _createNewBranch, value, true)) { if (value) - SelectedBranch = string.Empty; + SelectedBranch = null; else - SelectedBranch = LocalBranches.Count > 0 ? LocalBranches[0] : string.Empty; + SelectedBranch = LocalBranches.Count > 0 ? LocalBranches[0] : null; } } } - public List LocalBranches + public List LocalBranches { get; private set; } - public List RemoteBranches + public Models.Branch SelectedBranch { - get; - private set; + get => _selectedBranch; + set => SetProperty(ref _selectedBranch, value); } - public string SelectedBranch + public string NewBranchName { - get => _selectedBranch; - set => SetProperty(ref _selectedBranch, value); + get => _newBranchName; + set => SetProperty(ref _newBranchName, value); } public bool SetTrackingBranch { get => _setTrackingBranch; - set => SetProperty(ref _setTrackingBranch, value); + set + { + if (SetProperty(ref _setTrackingBranch, value)) + AutoSelectTrackingBranch(); + } } - public string SelectedTrackingBranch + public List RemoteBranches { get; - set; + private set; + } + + public Models.Branch SelectedTrackingBranch + { + get => _selectedTrackingBranch; + set => SetProperty(ref _selectedTrackingBranch, value); } public AddWorktree(Repository repo) { _repo = repo; - LocalBranches = new List(); - RemoteBranches = new List(); + LocalBranches = new List(); + RemoteBranches = new List(); foreach (var branch in repo.Branches) { if (branch.IsLocal) - LocalBranches.Add(branch.Name); + { + if (!branch.IsCurrent && !branch.HasWorktree) + LocalBranches.Add(branch); + } else - RemoteBranches.Add(branch.FriendlyName); + { + RemoteBranches.Add(branch); + } } - - if (RemoteBranches.Count > 0) - SelectedTrackingBranch = RemoteBranches[0]; - else - SelectedTrackingBranch = string.Empty; } public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) @@ -109,8 +120,8 @@ public override async Task Sure() using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding worktree ..."; - var branchName = _selectedBranch; - var tracking = _setTrackingBranch ? SelectedTrackingBranch : string.Empty; + var branchName = GetBranchName(false); + var tracking = (_setTrackingBranch && _selectedTrackingBranch != null) ? _selectedTrackingBranch.FriendlyName : string.Empty; var log = _repo.CreateLog("Add Worktree"); Use(log); @@ -123,10 +134,51 @@ public override async Task Sure() return succ; } + private void AutoSelectTrackingBranch() + { + if (!_setTrackingBranch || RemoteBranches.Count == 0) + return; + + var name = GetBranchName(true); + if (!string.IsNullOrEmpty(name)) + { + var remoteBranch = RemoteBranches.Find(b => b.Name.Equals(name, StringComparison.Ordinal)); + remoteBranch ??= RemoteBranches.Find(b => b.Name.EndsWith("/" + name, StringComparison.Ordinal)); + if (remoteBranch != null) + { + SelectedTrackingBranch = remoteBranch; + return; + } + } + + SelectedTrackingBranch = RemoteBranches[0]; + } + + private string GetBranchName(bool fallback) + { + do + { + if (!_createNewBranch) + { + if (_selectedBranch != null) + return _selectedBranch.Name; + + break; + } + + if (!string.IsNullOrEmpty(_newBranchName)) + return _newBranchName; + } while (false); + + return fallback ? System.IO.Path.GetFileName(_path.TrimEnd('/', '\\')) : string.Empty; + } + private Repository _repo = null; private string _path = string.Empty; private bool _createNewBranch = true; - private string _selectedBranch = string.Empty; + private Models.Branch _selectedBranch = null; + private string _newBranchName = string.Empty; private bool _setTrackingBranch = false; + private Models.Branch _selectedTrackingBranch = null; } } diff --git a/src/ViewModels/Apply.cs b/src/ViewModels/Apply.cs index 3eab5ef7a..14ecc0730 100644 --- a/src/ViewModels/Apply.cs +++ b/src/ViewModels/Apply.cs @@ -1,12 +1,14 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; +using Avalonia.Input.Platform; + namespace SourceGit.ViewModels { public class Apply : Popup { - [Required(ErrorMessage = "Patch file is required!!!")] [CustomValidation(typeof(Apply), nameof(ValidatePatchFile))] public string PatchFile { @@ -26,19 +28,45 @@ public Models.ApplyWhiteSpaceMode SelectedWhiteSpaceMode set; } + public bool FromClipboard + { + get => _fromClipboard; + set => SetProperty(ref _fromClipboard, value); + } + + public bool ThreeWayMerge + { + get; + set; + } + + public IClipboard Clipboard + { + get; + set; + } = null; + public Apply(Repository repo) { _repo = repo; - SelectedWhiteSpaceMode = Models.ApplyWhiteSpaceMode.Supported[0]; } - public static ValidationResult ValidatePatchFile(string file, ValidationContext _) + public static ValidationResult ValidatePatchFile(string file, ValidationContext ctx) { - if (File.Exists(file)) + if (ctx.ObjectInstance is not Apply apply) + return new ValidationResult("Invalid object instance!!!"); + + if (apply.FromClipboard) return ValidationResult.Success; - return new ValidationResult($"File '{file}' can NOT be found!!!"); + if (string.IsNullOrEmpty(file)) + return new ValidationResult("Please select a patch file!!!"); + + if (!File.Exists(file)) + return new ValidationResult($"File '{file}' can NOT be found!!!"); + + return ValidationResult.Success; } public override async Task Sure() @@ -46,19 +74,45 @@ public override async Task Sure() using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Apply patch..."; + var finalPatchFile = _patchFile; + if (_fromClipboard) + { + if (Clipboard == null) + { + _repo.SendNotification("Clipboard service is not available!!!", true); + return false; + } + + var content = await Clipboard.TryGetTextAsync(); + if (string.IsNullOrEmpty(content) || !content.StartsWith("diff --git ", StringComparison.Ordinal)) + { + _repo.SendNotification("There's no valid patch content in clipboard!!!", true); + return false; + } + + finalPatchFile = Path.GetTempFileName(); + File.WriteAllText(finalPatchFile, content); + } + var log = _repo.CreateLog("Apply Patch"); Use(log); - var succ = await new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, null) + var extra = ThreeWayMerge ? "--3way" : string.Empty; + var succ = await new Commands.Apply(_repo.FullPath, finalPatchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, extra) .Use(log) .ExecAsync(); log.Complete(); + + if (_fromClipboard) + File.Delete(finalPatchFile); + return succ; } private readonly Repository _repo = null; private string _patchFile = string.Empty; + private bool _fromClipboard = false; private bool _ignoreWhiteSpace = true; } } diff --git a/src/ViewModels/Archive.cs b/src/ViewModels/Archive.cs index 182ec562b..7f5840eec 100644 --- a/src/ViewModels/Archive.cs +++ b/src/ViewModels/Archive.cs @@ -59,7 +59,7 @@ public override async Task Sure() log.Complete(); if (succ) - App.SendNotification(_repo.FullPath, $"Save archive to : {_saveFile}"); + _repo.SendNotification($"Save archive to : {_saveFile}"); return succ; } diff --git a/src/ViewModels/Blame.cs b/src/ViewModels/Blame.cs index fda29964c..be05fccd4 100644 --- a/src/ViewModels/Blame.cs +++ b/src/ViewModels/Blame.cs @@ -10,9 +10,20 @@ namespace SourceGit.ViewModels { public class Blame : ObservableObject { - public string FilePath + public string File { - get; + get => _file; + private set => SetProperty(ref _file, value); + } + + public bool IgnoreWhitespace + { + get => _ignoreWhitespace; + set + { + if (SetProperty(ref _ignoreWhitespace, value)) + SetBlameData(_navigationHistory[0]); + } } public Models.Commit Revision @@ -55,14 +66,9 @@ public bool CanForward public Blame(string repo, string file, Models.Commit commit) { var sha = commit.SHA.Substring(0, 10); - - FilePath = file; - Revision = commit; - PrevRevision = null; - _repo = repo; - _navigationHistory.Add(sha); - SetBlameData(sha); + _navigationHistory.Add(new RevisionInfo(file, sha)); + SetBlameData(_navigationHistory[0]); } public string GetCommitMessage(string sha) @@ -83,7 +89,7 @@ public void Back() _navigationActiveIndex--; OnPropertyChanged(nameof(CanBack)); OnPropertyChanged(nameof(CanForward)); - NavigateToCommit(_navigationHistory[_navigationActiveIndex], true); + NavigateToCommit(_navigationHistory[_navigationActiveIndex]); } public void Forward() @@ -94,50 +100,63 @@ public void Forward() _navigationActiveIndex++; OnPropertyChanged(nameof(CanBack)); OnPropertyChanged(nameof(CanForward)); - NavigateToCommit(_navigationHistory[_navigationActiveIndex], true); + NavigateToCommit(_navigationHistory[_navigationActiveIndex]); } public void GotoPrevRevision() { - if (_prevRevision == null) - return; - - NavigateToCommit(_prevRevision.SHA, false); + if (_prevRevision != null) + NavigateToCommit(_file, _prevRevision.SHA.Substring(0, 10)); } - public void NavigateToCommit(string commitSHA, bool isBackOrForward) + public void NavigateToCommit(string file, string sha) { - if (Revision.SHA.StartsWith(commitSHA, StringComparison.Ordinal)) - return; - - if (!isBackOrForward) + if (App.GetLauncher() is { Pages: { } pages }) { - var count = _navigationHistory.Count; - if (_navigationActiveIndex < count - 1) - _navigationHistory.RemoveRange(_navigationActiveIndex + 1, count - _navigationActiveIndex - 1); - - _navigationHistory.Add(commitSHA); - _navigationActiveIndex++; - OnPropertyChanged(nameof(CanBack)); - OnPropertyChanged(nameof(CanForward)); + foreach (var page in pages) + { + if (page.Data is Repository repo && repo.FullPath.Equals(_repo)) + { + repo.NavigateToCommit(sha); + break; + } + } } - SetBlameData(commitSHA); + if (Revision.SHA.StartsWith(sha, StringComparison.Ordinal)) + return; + + var count = _navigationHistory.Count; + if (_navigationActiveIndex < count - 1) + _navigationHistory.RemoveRange(_navigationActiveIndex + 1, count - _navigationActiveIndex - 1); + var rev = new RevisionInfo(file, sha); + _navigationHistory.Add(rev); + _navigationActiveIndex++; + OnPropertyChanged(nameof(CanBack)); + OnPropertyChanged(nameof(CanForward)); + SetBlameData(rev); + } + + private void NavigateToCommit(RevisionInfo rev) + { if (App.GetLauncher() is { Pages: { } pages }) { foreach (var page in pages) { if (page.Data is Repository repo && repo.FullPath.Equals(_repo)) { - repo.NavigateToCommit(commitSHA); + repo.NavigateToCommit(rev.SHA); break; } } } + + if (!Revision.SHA.StartsWith(rev.SHA, StringComparison.Ordinal)) + SetBlameData(rev); } - private void SetBlameData(string commitSHA) + private void SetBlameData(RevisionInfo rev) { if (_cancellationSource is { IsCancellationRequested: false }) _cancellationSource.Cancel(); @@ -145,14 +164,16 @@ private void SetBlameData(string commitSHA) _cancellationSource = new CancellationTokenSource(); var token = _cancellationSource.Token; + File = rev.File; + Task.Run(async () => { var argsBuilder = new StringBuilder(); argsBuilder .Append("--date-order -n 2 ") - .Append(commitSHA ?? string.Empty) + .Append(rev.SHA) .Append(" -- ") - .Append(FilePath.Quoted()); + .Append(rev.File.Quoted()); var commits = await new Commands.QueryCommits(_repo, argsBuilder.ToString(), false) .GetResultAsync() @@ -163,14 +184,14 @@ private void SetBlameData(string commitSHA) if (!token.IsCancellationRequested) { Revision = commits.Count > 0 ? commits[0] : null; - PrevRevision = commits.Count == 2 ? commits[1] : null; + PrevRevision = commits.Count > 1 ? commits[1] : null; } }); }); Task.Run(async () => { - var result = await new Commands.Blame(_repo, FilePath, commitSHA) + var result = await new Commands.Blame(_repo, rev.File, rev.SHA, _ignoreWhitespace) .ReadAsync() .ConfigureAwait(false); @@ -182,12 +203,26 @@ private void SetBlameData(string commitSHA) }, token); } + private class RevisionInfo + { + public string File { get; set; } = string.Empty; + public string SHA { get; set; } = string.Empty; + + public RevisionInfo(string file, string sha) + { + File = file; + SHA = sha; + } + } + private string _repo; + private string _file; + private bool _ignoreWhitespace = false; private Models.Commit _revision; private Models.Commit _prevRevision; private CancellationTokenSource _cancellationSource = null; private int _navigationActiveIndex = 0; - private List _navigationHistory = []; + private List _navigationHistory = []; private Models.BlameData _data = null; private Dictionary _commitMessages = new(); } diff --git a/src/ViewModels/BlameCommandPalette.cs b/src/ViewModels/BlameCommandPalette.cs index fa24faf6a..e73430872 100644 --- a/src/ViewModels/BlameCommandPalette.cs +++ b/src/ViewModels/BlameCommandPalette.cs @@ -35,9 +35,8 @@ public string SelectedFile set => SetProperty(ref _selectedFile, value); } - public BlameCommandPalette(Launcher launcher, string repo) + public BlameCommandPalette(string repo) { - _launcher = launcher; _repo = repo; _isLoading = true; @@ -61,27 +60,17 @@ public BlameCommandPalette(Launcher launcher, string repo) }); } - 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() + public Blame Launch() { - if (!string.IsNullOrEmpty(_selectedFile)) - App.ShowWindow(new Blame(_repo, _selectedFile, _head)); - _launcher.CancelCommandPalette(); + _repoFiles.Clear(); + _visibleFiles.Clear(); + Close(); + return !string.IsNullOrEmpty(_selectedFile) ? new Blame(_repo, _selectedFile, _head) : null; } private void UpdateVisible() @@ -117,7 +106,6 @@ private void UpdateVisible() } } - private Launcher _launcher = null; private string _repo = null; private bool _isLoading = false; private Models.Commit _head = null; diff --git a/src/ViewModels/BlockNavigation.cs b/src/ViewModels/BlockNavigation.cs index 96f47146b..0debfba0e 100644 --- a/src/ViewModels/BlockNavigation.cs +++ b/src/ViewModels/BlockNavigation.cs @@ -110,6 +110,29 @@ public Block Goto(BlockNavigationDirection direction) return _blocks[_current]; } + public void UpdateByChunk(TextDiffSelectedChunk chunk) + { + _current = -1; + + var chunkStart = chunk.StartIdx + 1; + var chunkEnd = chunk.EndIdx + 1; + + for (var i = 0; i < _blocks.Count; i++) + { + var block = _blocks[i]; + if (chunkStart > block.End) + continue; + + if (chunkEnd < block.Start) + { + _current = i - 1; + break; + } + + _current = i; + } + } + public void UpdateByCaretPosition(int caretLine) { if (_current >= 0 && _current < _blocks.Count) diff --git a/src/ViewModels/BranchCompareCommandPalette.cs b/src/ViewModels/BranchCompareCommandPalette.cs deleted file mode 100644 index bf2f2fc15..000000000 --- a/src/ViewModels/BranchCompareCommandPalette.cs +++ /dev/null @@ -1,98 +0,0 @@ -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/Checkout.cs b/src/ViewModels/Checkout.cs index ec67dca31..3c47d47a3 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -4,41 +4,39 @@ namespace SourceGit.ViewModels { public class Checkout : Popup { - public string Branch + public string BranchName { - get; - } - - public bool DiscardLocalChanges - { - get; - set; + get => _branch.Name; } - public bool IsRecurseSubmoduleVisible + public bool HasLocalChanges { - get => _repo.Submodules.Count > 0; + get => _repo.LocalChangesCount > 0; } - public bool RecurseSubmodules + public Models.DealWithLocalChanges DealWithLocalChanges { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; + get; + set; } - public Checkout(Repository repo, string branch) + public Checkout(Repository repo, Models.Branch branch) { _repo = repo; - Branch = branch; - DiscardLocalChanges = false; + _branch = branch; + + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = $"Checkout '{Branch}' ..."; + var branchName = BranchName; + ProgressDescription = $"Checkout '{branchName}' ..."; - var log = _repo.CreateLog($"Checkout '{Branch}'"); + var log = _repo.CreateLog($"Checkout '{branchName}'"); Use(log); if (_repo.CurrentBranch is { IsDetachedHead: true }) @@ -56,58 +54,62 @@ public override async Task Sure() var succ = false; var needPopStash = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(branchName, false); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) .Use(log) - .PushAsync("CHECKOUT_AUTO_STASH"); + .PushAsync("CHECKOUT_AUTO_STASH", false); if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .BranchAsync(Branch, DiscardLocalChanges); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(branchName, false); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(branchName, true); + } 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); - } + await _repo.AutoUpdateSubmodulesAsync(log); if (needPopStash) await new Commands.Stash(_repo.FullPath) .Use(log) .PopAsync("stash@{0}"); + + _repo.RefreshAfterCheckoutBranch(_branch); + } + else + { + _repo.MarkWorkingCopyDirtyManually(); } log.Complete(); - - var b = _repo.Branches.Find(x => x.IsLocal && x.Name == Branch); - if (b != null && _repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(b, Models.FilterMode.Included, false, false); - - _repo.MarkBranchesDirtyManually(); - - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); return succ; } private readonly Repository _repo = null; + private readonly Models.Branch _branch = null; } } diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 63b93a9df..50dc99aa3 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -14,21 +14,15 @@ public Models.Branch RemoteBranch get; } - public bool DiscardLocalChanges + public bool HasLocalChanges { - get; - set; - } - - public bool IsRecurseSubmoduleVisible - { - get => _repo.Submodules.Count > 0; + get => _repo.LocalChangesCount > 0; } - public bool RecurseSubmodules + public Models.DealWithLocalChanges DealWithLocalChanges { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; + get; + set; } public CheckoutAndFastForward(Repository repo, Models.Branch localBranch, Models.Branch remoteBranch) @@ -36,6 +30,10 @@ public CheckoutAndFastForward(Repository repo, Models.Branch localBranch, Models _repo = repo; LocalBranch = localBranch; RemoteBranch = remoteBranch; + + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() @@ -61,54 +59,62 @@ public override async Task Sure() var succ = false; var needPopStash = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(LocalBranch.Name, RemoteBranch.Head, false, true); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) .Use(log) - .PushAsync("CHECKOUT_AND_FASTFORWARD_AUTO_STASH"); + .PushAsync("CHECKOUT_AND_FASTFORWARD_AUTO_STASH", false); if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .BranchAsync(LocalBranch.Name, RemoteBranch.Head, DiscardLocalChanges, true); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(LocalBranch.Name, RemoteBranch.Head, false, true); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(LocalBranch.Name, RemoteBranch.Head, true, true); + } 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); - } + await _repo.AutoUpdateSubmodulesAsync(log); if (needPopStash) await new Commands.Stash(_repo.FullPath) .Use(log) .PopAsync("stash@{0}"); - } - log.Complete(); + LocalBranch.Behind.Clear(); + LocalBranch.Head = RemoteBranch.Head; + LocalBranch.CommitterDate = RemoteBranch.CommitterDate; - if (_repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(LocalBranch, Models.FilterMode.Included, false, false); - - _repo.MarkBranchesDirtyManually(); + _repo.RefreshAfterCheckoutBranch(LocalBranch); + } + else + { + _repo.MarkWorkingCopyDirtyManually(); + } - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); + log.Complete(); return succ; } diff --git a/src/ViewModels/CheckoutBranchFromStash.cs b/src/ViewModels/CheckoutBranchFromStash.cs new file mode 100644 index 000000000..d62f4f2df --- /dev/null +++ b/src/ViewModels/CheckoutBranchFromStash.cs @@ -0,0 +1,70 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutBranchFromStash : Popup + { + public Models.Stash Target + { + get; + } + + [Required(ErrorMessage = "Branch name is required!")] + [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] + [CustomValidation(typeof(CheckoutBranchFromStash), nameof(ValidateBranchName))] + public string BranchName + { + get => _branchName; + set => SetProperty(ref _branchName, value, true); + } + + public CheckoutBranchFromStash(Repository repo, Models.Stash stash) + { + _repo = repo; + Target = stash; + } + + public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) + { + if (ctx.ObjectInstance is CheckoutBranchFromStash caller) + { + foreach (var b in caller._repo.Branches) + { + if (b.FriendlyName.Equals(name, StringComparison.Ordinal)) + return new ValidationResult("A branch with same name already exists!"); + } + + return ValidationResult.Success; + } + + return new ValidationResult("Missing runtime context to create branch!"); + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = "Checkout branch from stash..."; + + var log = _repo.CreateLog($"Checkout Branch '{_branchName}'"); + Use(log); + + var succ = await new Commands.Stash(_repo.FullPath) + .Use(log) + .CheckoutBranchAsync(Target.Name, _branchName); + + if (succ) + { + _repo.MarkWorkingCopyDirtyManually(); + _repo.MarkStashesDirtyManually(); + } + + log.Complete(); + return true; + } + + private readonly Repository _repo; + private string _branchName = string.Empty; + } +} diff --git a/src/ViewModels/CheckoutCommandPalette.cs b/src/ViewModels/CheckoutCommandPalette.cs index 3e25ebc9e..480556bc5 100644 --- a/src/ViewModels/CheckoutCommandPalette.cs +++ b/src/ViewModels/CheckoutCommandPalette.cs @@ -28,22 +28,12 @@ public string Filter } } - public CheckoutCommandPalette(Launcher launcher, Repository repo) + public CheckoutCommandPalette(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; @@ -51,13 +41,11 @@ public void ClearFilter() public async Task ExecAsync() { - _launcher.CommandPalette = null; + _branches.Clear(); + Close(); if (_selectedBranch != null) await _repo.CheckoutBranchAsync(_selectedBranch); - - Dispose(); - GC.Collect(); } private void UpdateBranches() @@ -79,7 +67,7 @@ private void UpdateBranches() branches.Sort((l, r) => { if (l.IsLocal == r.IsLocal) - return l.Name.CompareTo(r.Name); + return Models.NumericSort.Compare(l.Name, r.Name); return l.IsLocal ? -1 : 1; }); @@ -94,7 +82,6 @@ private void UpdateBranches() SelectedBranch = autoSelected; } - private Launcher _launcher; private Repository _repo; private List _branches = []; private Models.Branch _selectedBranch = null; diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs deleted file mode 100644 index b4d4438c7..000000000 --- a/src/ViewModels/CheckoutCommit.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Threading.Tasks; - -namespace SourceGit.ViewModels -{ - public class CheckoutCommit : Popup - { - public Models.Commit Commit - { - get; - } - - public bool DiscardLocalChanges - { - get; - 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; - Commit = commit; - DiscardLocalChanges = false; - } - - public override async Task Sure() - { - using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = $"Checkout Commit '{Commit.SHA}' ..."; - - var log = _repo.CreateLog("Checkout Commit"); - Use(log); - - if (_repo.CurrentBranch is { IsDetachedHead: true }) - { - var refs = await new Commands.QueryRefsContainsCommit(_repo.FullPath, _repo.CurrentBranch.Head).GetResultAsync(); - if (refs.Count == 0) - { - var msg = App.Text("Checkout.WarnLostCommits"); - var shouldContinue = await App.AskConfirmAsync(msg); - if (!shouldContinue) - return true; - } - } - - var succ = false; - var needPop = false; - - if (!DiscardLocalChanges) - { - var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); - if (changes > 0) - { - succ = await new Commands.Stash(_repo.FullPath) - .Use(log) - .PushAsync("CHECKOUT_AUTO_STASH"); - if (!succ) - { - log.Complete(); - return false; - } - - needPop = true; - } - } - - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .CommitAsync(Commit.SHA, DiscardLocalChanges); - - 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); - } - - if (needPop) - await new Commands.Stash(_repo.FullPath) - .Use(log) - .PopAsync("stash@{0}"); - } - - log.Complete(); - return succ; - } - - private readonly Repository _repo = null; - } -} diff --git a/src/ViewModels/CheckoutDetached.cs b/src/ViewModels/CheckoutDetached.cs new file mode 100644 index 000000000..4e4d0def0 --- /dev/null +++ b/src/ViewModels/CheckoutDetached.cs @@ -0,0 +1,120 @@ +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutDetached : Popup + { + public object Target + { + get; + } + + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + + public Models.DealWithLocalChanges DealWithLocalChanges + { + get; + set; + } + + public CheckoutDetached(Repository repo, Models.Commit commit) + { + _repo = repo; + _revision = commit.SHA; + + Target = commit; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; + } + + public CheckoutDetached(Repository repo, Models.Tag tag) + { + _repo = repo; + _revision = tag.SHA; + + Target = tag; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Checkout Commit '{_revision}' ..."; + + var log = _repo.CreateLog("Checkout Commit"); + Use(log); + + if (_repo.CurrentBranch is { IsDetachedHead: true }) + { + var refs = await new Commands.QueryRefsContainsCommit(_repo.FullPath, _repo.CurrentBranch.Head).GetResultAsync(); + if (refs.Count == 0) + { + var msg = App.Text("Checkout.WarnLostCommits"); + var shouldContinue = await App.AskConfirmAsync(msg); + if (!shouldContinue) + return true; + } + } + + var succ = false; + var needPop = false; + + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .CommitAsync(_revision, false); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) + { + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); + if (changes > 0) + { + succ = await new Commands.Stash(_repo.FullPath) + .Use(log) + .PushAsync("CHECKOUT_AUTO_STASH", false); + if (!succ) + { + log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); + return false; + } + + needPop = true; + } + + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .CommitAsync(_revision, false); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .CommitAsync(_revision, true); + } + + if (succ) + { + await _repo.AutoUpdateSubmodulesAsync(log); + + if (needPop) + await new Commands.Stash(_repo.FullPath) + .Use(log) + .PopAsync("stash@{0}"); + } + + log.Complete(); + return succ; + } + + private readonly Repository _repo = null; + private readonly string _revision = string.Empty; + } +} diff --git a/src/ViewModels/CherryPick.cs b/src/ViewModels/CherryPick.cs index fb8ae649f..55ec83c99 100644 --- a/src/ViewModels/CherryPick.cs +++ b/src/ViewModels/CherryPick.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.IO; +using System.Text; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -93,6 +95,19 @@ public override async Task Sure() string.Empty) .Use(log) .ExecAsync(); + + if (!AutoCommit && Targets.Count > 1) + { + var builder = new StringBuilder(); + foreach (var t in Targets) + builder.Append(t.Subject).Append("\n"); + + builder.Append("\n"); + foreach (var t in Targets) + builder.Append("(cherry picked from commit ").Append(t.SHA).Append(")\n"); + + File.WriteAllText(Path.Combine(_repo.GitDir, "MERGE_MSG"), builder.ToString()); + } } log.Complete(); diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index 73033682e..e62937cbc 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; -using Avalonia.Threading; namespace SourceGit.ViewModels { @@ -46,6 +46,23 @@ public string Local set => SetProperty(ref _local, value); } + public List Groups + { + get; + } + + public RepositoryNode SelectedGroup + { + get => _selectedGroup; + set => SetProperty(ref _selectedGroup, value); + } + + public int Bookmark + { + get => _bookmark; + set => SetProperty(ref _bookmark, value); + } + public string ExtraArgs { get => _extraArgs; @@ -62,24 +79,15 @@ public Clone(string pageId) { _pageId = pageId; + Groups = new List(); + Groups.Add(new RepositoryNode { Name = "No Group (Uncategorized)", Id = string.Empty }); + SelectedGroup = Groups[0]; + CollectGroups(Groups, Preferences.Instance.RepositoryNodes); + var activeWorkspace = Preferences.Instance.GetActiveWorkspace(); _parentFolder = activeWorkspace?.DefaultCloneDir; if (string.IsNullOrEmpty(ParentFolder)) _parentFolder = Preferences.Instance.GitDefaultCloneDir; - - Task.Run(async () => - { - try - { - var text = await App.GetClipboardTextAsync(); - if (Models.Remote.IsValidURL(text)) - Dispatcher.UIThread.Post(() => Remote = text); - } - catch - { - // Ignore - } - }); } public static ValidationResult ValidateRemote(string remote, ValidationContext _) @@ -127,7 +135,7 @@ public override async Task Sure() if (!Directory.Exists(path)) { - App.RaiseException(_pageId, $"Folder '{path}' can NOT be found"); + Models.Notification.Send(_pageId, $"Folder '{path}' can NOT be found", true); return false; } @@ -140,16 +148,18 @@ 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) - .UpdateAsync(submodules, true, true); + .UpdateAsync(submodules, true, true, false); } log.Complete(); - var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, null, true); + var parent = _selectedGroup is { Id: not "" } ? _selectedGroup : null; + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, parent, true); + node.Bookmark = _bookmark; await node.UpdateStatusAsync(false, null); var launcher = App.GetLauncher(); @@ -168,6 +178,18 @@ public override async Task Sure() return true; } + private void CollectGroups(List outs, List collections) + { + foreach (var node in collections) + { + if (!node.IsRepository) + { + outs.Add(node); + CollectGroups(outs, node.SubNodes); + } + } + } + private string _pageId = string.Empty; private string _remote = string.Empty; private bool _useSSH = false; @@ -175,5 +197,7 @@ public override async Task Sure() private string _parentFolder = string.Empty; private string _local = string.Empty; private string _extraArgs = string.Empty; + private RepositoryNode _selectedGroup = null; + private int _bookmark = 0; } } diff --git a/src/ViewModels/CommandLog.cs b/src/ViewModels/CommandLog.cs index f12d27b45..191f0f6c5 100644 --- a/src/ViewModels/CommandLog.cs +++ b/src/ViewModels/CommandLog.cs @@ -63,7 +63,10 @@ public void AppendLine(string line = null) else { var newline = line ?? string.Empty; - _builder.AppendLine(newline); + if (_builder != null) + _builder.AppendLine(newline); + else + _content = $"{_content}{newline}\n"; foreach (var receiver in _receivers) receiver.OnReceiveCommandLog(newline); diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 0c2f16785..b23465774 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; @@ -7,7 +7,6 @@ using Avalonia; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -26,7 +25,7 @@ public CommitDetailSharedData() } } - public partial class CommitDetail : ObservableObject, IDisposable + public partial class CommitDetail : ObservableObject { public Repository Repository { @@ -41,6 +40,7 @@ public int ActiveTabIndex if (value != _sharedData.ActiveTabIndex) { _sharedData.ActiveTabIndex = value; + OnPropertyChanged(nameof(ActiveTabIndex)); if (value == 1 && DiffContext == null && _selectedChanges is { Count: 1 }) DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, _selectedChanges[0])); @@ -53,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(); } @@ -76,12 +79,6 @@ public List WebLinks private set; } - public List Children - { - get => _children; - private set => SetProperty(ref _children, value); - } - public List Changes { get => _changes; @@ -172,21 +169,12 @@ public CommitDetail(Repository repo, CommitDetailSharedData sharedData) WebLinks = Models.CommitLink.Get(repo.Remotes); } - public void Dispose() + public CommitDetail Clone() { - _repo = null; - _commit = null; - _changes = null; - _visibleChanges = null; - _selectedChanges = null; - _signInfo = null; - _searchChangeFilter = null; - _diffContext = null; - _viewRevisionFileContent = null; - _cancellationSource = null; - _requestingRevisionFiles = false; - _revisionFiles = null; - _revisionFileSearchSuggestion = null; + var cloned = new CommitDetail(_repo, null); + cloned.ActiveTabIndex = ActiveTabIndex; + cloned.Commit = _commit; + return cloned; } public void NavigateTo(string commitSHA) @@ -238,61 +226,179 @@ public async Task SaveChangesAsPatchAsync(List changes, string sa 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); + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync( + _repo.FullPath, + changes, + _commit.FirstParentToCompare, + _commit.SHA, + saveTo); + if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); + _repo.SendNotification(App.Text("SaveAsPatchSuccess")); } public async Task ResetToThisRevisionAsync(string path) { + var c = _changes?.Find(x => x.Path.Equals(path, StringComparison.Ordinal)); + if (c != null) + { + await ResetToThisRevisionAsync(c); + return; + } + 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 ResetToThisRevisionAsync(Models.Change change) + { + var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); + + if (change.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, change.OriginalPath); + if (File.Exists(old)) + await new Commands.Remove(_repo.FullPath, [change.OriginalPath]) + .Use(log) + .ExecAsync(); + + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, _commit.SHA); + } + else + { + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.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"); + if (change.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(renamed)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1"); + } + else + { + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, $"{_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(); + var checkouts = new List(); + var removes = new List(); + foreach (var c in changes) - files.Add(c.Path); + { + if (c.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, c.OriginalPath); + if (File.Exists(old)) + removes.Add(c.OriginalPath); + + checkouts.Add(c.Path); + } + else + { + checkouts.Add(c.Path); + } + } var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'"); - await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA); + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes) + .Use(log) + .ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(checkouts, _commit.SHA); + log.Complete(); } public async Task ResetMultipleToParentRevisionAsync(List changes) { - var renamed = new List(); - var modified = new List(); + var checkouts = new List(); + var removes = new List(); foreach (var c in changes) { - if (c.Index == Models.ChangeState.Renamed) - renamed.Add(c.OriginalPath); + if (c.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(renamed)) + removes.Add(c.Path); + + checkouts.Add(c.OriginalPath); + } else - modified.Add(c.Path); + { + checkouts.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 (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes) + .Use(log) + .ExecAsync(); - if (renamed.Count > 0) - await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1"); + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(checkouts, $"{_commit.SHA}~1"); log.Complete(); } @@ -340,7 +446,7 @@ await Commands.SaveRevisionFile if (tool == null) Native.OS.OpenWithDefaultEditor(tmpFile); else - tool.Open(tmpFile); + tool.Launch(tmpFile.Quoted()); } public async Task SaveRevisionFileAsync(Models.Object file, string saveTo) @@ -352,7 +458,6 @@ await Commands.SaveRevisionFile private void Refresh() { - _changes = []; _requestingRevisionFiles = false; _revisionFiles = null; @@ -360,13 +465,17 @@ private void Refresh() ViewRevisionFileContent = null; ViewRevisionFilePath = string.Empty; CanOpenRevisionFileWithDefaultEditor = false; - Children = null; RevisionFileSearchFilter = string.Empty; RevisionFileSearchSuggestion = null; ScrollOffset = Vector.Zero; if (_commit == null) + { + Changes = []; + VisibleChanges = []; + SelectedChanges = null; return; + } if (_cancellationSource is { IsCancellationRequested: false }) _cancellationSource.Cancel(); @@ -402,23 +511,13 @@ private void Refresh() Dispatcher.UIThread.Post(() => SignInfo = signInfo); }, token); - if (Preferences.Instance.ShowChildren) - { - Task.Run(async () => - { - var max = Preferences.Instance.MaxHistoryCommits; - var cmd = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, max) { CancellationToken = token }; - var children = await cmd.GetResultAsync().ConfigureAwait(false); - if (!token.IsCancellationRequested) - Dispatcher.UIThread.Post(() => Children = children); - }, token); - } - Task.Run(async () => { - 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 changes = await new Commands.CompareRevisions(_repo.FullPath, _commit.FirstParentToCompare, _commit.SHA) + .WithCancellation(token) + .ReadAsync() + .ConfigureAwait(false); + var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) { @@ -482,6 +581,17 @@ private void Refresh() inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha)); } + var inlineCodeMatches = REG_INLINECODE_FORMAT().Matches(message); + foreach (Match match in inlineCodeMatches) + { + var start = match.Index; + var len = match.Length; + if (inlines.Intersect(start, len) != null) + continue; + + inlines.Add(new Models.InlineElement(Models.InlineElementType.Code, start + 1, len - 2, string.Empty)); + } + inlines.Sort(); return inlines; } @@ -603,39 +713,29 @@ private async Task SetViewingBlobAsync(Models.Object file) private async Task SetViewingCommitAsync(Models.Object file) { - var submoduleRoot = Path.Combine(_repo.FullPath, file.Path).Replace('\\', '/').Trim('/'); - var commit = await new Commands.QuerySingleCommit(submoduleRoot, file.SHA).GetResultAsync(); - if (commit == null) + var submoduleRoot = Path.Combine(_repo.FullPath, file.Path).Replace('\\', '/').TrimEnd('/'); + var info = await new Commands.QuerySubmoduleRevision(submoduleRoot, file.SHA).GetResultAsync(); + ViewRevisionFileContent = info ?? new Models.RevisionSubmodule() { - ViewRevisionFileContent = new Models.RevisionSubmodule() - { - Commit = new Models.Commit() { SHA = file.SHA }, - FullMessage = new Models.CommitFullMessage() - }; - } - else - { - var message = await new Commands.QueryCommitFullMessage(submoduleRoot, file.SHA).GetResultAsync(); - ViewRevisionFileContent = new Models.RevisionSubmodule() - { - Commit = commit, - FullMessage = new Models.CommitFullMessage { Message = message } - }; - } + Commit = new Models.Commit() { SHA = file.SHA }, + FullMessage = new Models.CommitFullMessage() + }; } [GeneratedRegex(@"\b(https?://|ftp://)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]")] private static partial Regex REG_URL_FORMAT(); - [GeneratedRegex(@"\b([0-9a-fA-F]{6,40})\b")] + [GeneratedRegex(@"\b([0-9a-fA-F]{6,64})\b")] private static partial Regex REG_SHA_FORMAT(); + [GeneratedRegex(@"`.*?`")] + private static partial Regex REG_INLINECODE_FORMAT(); + private Repository _repo = null; 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 = []; private List _visibleChanges = []; private List _selectedChanges = null; diff --git a/src/ViewModels/Compare.cs b/src/ViewModels/Compare.cs new file mode 100644 index 000000000..9a3f4f490 --- /dev/null +++ b/src/ViewModels/Compare.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class Compare : ObservableObject + { + public bool IsLoadingChanges + { + get => _isLoadingChanges; + private set => SetProperty(ref _isLoadingChanges, value); + } + + public bool IsLoadingPickableCommits + { + get => _isLoadingPickableCommits; + private set => SetProperty(ref _isLoadingPickableCommits, value); + } + + public bool IsViewChanges + { + get => _isViewChanges; + set => SetProperty(ref _isViewChanges, value); + } + + public bool CanResetFiles + { + get => _canResetFiles; + } + + public string BaseName + { + get => _baseName; + private set => SetProperty(ref _baseName, value); + } + + public string ToName + { + get => _toName; + private set => SetProperty(ref _toName, value); + } + + public Models.Commit BaseHead + { + get => _baseHead; + private set => SetProperty(ref _baseHead, value); + } + + public Models.Commit ToHead + { + get => _toHead; + private set => SetProperty(ref _toHead, value); + } + + public int TotalChanges + { + get => _totalChanges; + private set => SetProperty(ref _totalChanges, value); + } + + public List VisibleChanges + { + get => _visibleChanges; + private set => SetProperty(ref _visibleChanges, value); + } + + public List SelectedChanges + { + get => _selectedChanges; + set + { + if (SetProperty(ref _selectedChanges, value)) + { + if (value is { Count: 1 }) + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_based, _to, value[0]), _diffContext); + else + DiffContext = null; + } + } + } + + public string SearchFilter + { + get => _searchFilter; + set + { + if (SetProperty(ref _searchFilter, value)) + RefreshVisibleChanges(); + } + } + + public DiffContext DiffContext + { + get => _diffContext; + private set => SetProperty(ref _diffContext, value); + } + + public List LeftOnlyCommits + { + get => _leftOnlyCommits; + private set => SetProperty(ref _leftOnlyCommits, value); + } + + public List RightOnlyCommits + { + get => _rightOnlyCommits; + private set => SetProperty(ref _rightOnlyCommits, value); + } + + public Compare(Repository repo, object based, object to) + { + _repo = repo; + _canResetFiles = !repo.IsBare; + _based = GetSHA(based); + _to = GetSHA(to); + _baseName = GetName(based); + _toName = GetName(to); + + _baseHead = new Commands.QuerySingleCommit(_repo.FullPath, _based).GetResult(); + _toHead = new Commands.QuerySingleCommit(_repo.FullPath, _to).GetResult(); + + UpdatePickableCommits(); + UpdateChanges(); + } + + public void NavigateTo(string commitSHA) + { + _repo.NavigateToCommit(commitSHA); + } + + public void Swap() + { + (_based, _to) = (_to, _based); + (BaseName, ToName) = (_toName, _baseName); + (BaseHead, ToHead) = (_toHead, _baseHead); + (LeftOnlyCommits, RightOnlyCommits) = (_rightOnlyCommits, _leftOnlyCommits); + UpdateChanges(); + } + + public void ClearSearchFilter() + { + SearchFilter = string.Empty; + } + + public string GetAbsPath(string path) + { + return Native.OS.GetAbsPath(_repo.FullPath, path); + } + + public void OpenInExternalDiffTool(Models.Change change) + { + new Commands.DiffTool(_repo.FullPath, new Models.DiffOption(_based, _to, change)).Open(); + } + + public async Task ResetToLeftAsync(Models.Change change) + { + if (!_canResetFiles) + return; + + if (change.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]).ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(renamed)) + await new Commands.Remove(_repo.FullPath, [change.Path]).ExecAsync(); + + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.OriginalPath, _baseHead.SHA); + } + else + { + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.Path, _baseHead.SHA); + } + } + + public async Task ResetToRightAsync(Models.Change change) + { + if (change.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]).ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, change.OriginalPath); + if (File.Exists(old)) + await new Commands.Remove(_repo.FullPath, [change.OriginalPath]).ExecAsync(); + + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.Path, ToHead.SHA); + } + else + { + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.Path, ToHead.SHA); + } + } + + public async Task ResetMultipleToLeftAsync(List changes) + { + var checkouts = new List(); + var removes = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(old)) + removes.Add(c.Path); + + checkouts.Add(c.OriginalPath); + } + else + { + checkouts.Add(c.Path); + } + } + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes).ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath).MultipleFilesWithRevisionAsync(checkouts, _baseHead.SHA); + } + + public async Task ResetMultipleToRightAsync(List changes) + { + var checkouts = new List(); + var removes = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, c.OriginalPath); + if (File.Exists(renamed)) + removes.Add(c.OriginalPath); + + checkouts.Add(c.Path); + } + else + { + checkouts.Add(c.Path); + } + } + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes).ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath).MultipleFilesWithRevisionAsync(checkouts, _toHead.SHA); + } + + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) + { + return await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes, _based, _to, saveTo); + } + + public void CherryPick(List commits) + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new CherryPick(_repo, commits)); + } + + private void UpdatePickableCommits() + { + IsLoadingPickableCommits = true; + + Task.Run(async () => + { + var rightOnly = await new Commands.QueryPickableCommits(_repo.FullPath, _baseHead.SHA, _toHead.SHA) + .GetResultAsync() + .ConfigureAwait(false); + + var leftOnly = await new Commands.QueryPickableCommits(_repo.FullPath, _toHead.SHA, _baseHead.SHA) + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + LeftOnlyCommits = leftOnly; + RightOnlyCommits = rightOnly; + IsLoadingPickableCommits = false; + }); + }); + } + + private void UpdateChanges() + { + IsLoadingChanges = true; + VisibleChanges = []; + SelectedChanges = []; + + Task.Run(async () => + { + _changes = await new Commands.CompareRevisions(_repo.FullPath, _based, _to) + .ReadAsync() + .ConfigureAwait(false); + + var visible = _changes; + if (!string.IsNullOrWhiteSpace(_searchFilter)) + { + visible = new List(); + foreach (var c in _changes) + { + if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } + } + + Dispatcher.UIThread.Post(() => + { + TotalChanges = _changes.Count; + VisibleChanges = visible; + IsLoadingChanges = false; + + if (VisibleChanges.Count > 0) + SelectedChanges = [VisibleChanges[0]]; + else + SelectedChanges = []; + }); + }); + } + + private void RefreshVisibleChanges() + { + if (_changes == null) + return; + + if (string.IsNullOrEmpty(_searchFilter)) + { + VisibleChanges = _changes; + } + else + { + var visible = new List(); + foreach (var c in _changes) + { + if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } + + VisibleChanges = visible; + } + } + + private string GetName(object obj) + { + return obj switch + { + Models.Branch b => b.FriendlyName, + Models.Tag t => t.Name, + Models.Commit c => c.SHA.Substring(0, 10), + _ => "HEAD", + }; + } + + private string GetSHA(object obj) + { + return obj switch + { + Models.Branch b => b.Head, + Models.Tag t => t.SHA, + Models.Commit c => c.SHA, + _ => "HEAD", + }; + } + + private Repository _repo; + private bool _isLoadingChanges = true; + private bool _isLoadingPickableCommits = true; + private bool _isViewChanges = true; + private bool _canResetFiles = false; + private string _based = string.Empty; + private string _to = string.Empty; + private string _baseName = string.Empty; + private string _toName = string.Empty; + 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; + private List _leftOnlyCommits = []; + private List _rightOnlyCommits = []; + private string _searchFilter = string.Empty; + private DiffContext _diffContext = null; + } +} diff --git a/src/ViewModels/CompareCommandPalette.cs b/src/ViewModels/CompareCommandPalette.cs new file mode 100644 index 000000000..7eb49efd2 --- /dev/null +++ b/src/ViewModels/CompareCommandPalette.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public class CompareCommandPalette : ICommandPalette + { + public object BasedOn + { + get => _basedOn; + } + + public object CompareTo + { + get => _compareTo; + set => SetProperty(ref _compareTo, value); + } + + public List Refs + { + get => _refs; + private set => SetProperty(ref _refs, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateRefs(); + } + } + + public CompareCommandPalette(Repository repo, object basedOn) + { + _repo = repo; + _basedOn = basedOn ?? repo.CurrentBranch; + UpdateRefs(); + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public Compare Launch() + { + _refs.Clear(); + Close(); + return _compareTo != null ? new Compare(_repo, _basedOn, _compareTo) : null; + } + + private void UpdateRefs() + { + var refs = new List(); + + foreach (var b in _repo.Branches) + { + if (b == _basedOn) + continue; + + if (string.IsNullOrEmpty(_filter) || b.FriendlyName.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + refs.Add(b); + } + + foreach (var t in _repo.Tags) + { + if (t == _basedOn) + continue; + + if (string.IsNullOrEmpty(_filter) || t.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + refs.Add(t); + } + + refs.Sort((l, r) => + { + if (l is Models.Branch lb) + { + if (r is Models.Branch rb) + { + if (lb.IsLocal == rb.IsLocal) + return Models.NumericSort.Compare(lb.FriendlyName, rb.FriendlyName); + return lb.IsLocal ? -1 : 1; + } + + return -1; + } + + if (r is Models.Branch) + return 1; + + return Models.NumericSort.Compare((l as Models.Tag).Name, (r as Models.Tag).Name); + }); + + var autoSelected = _compareTo; + if (refs.Count == 0) + autoSelected = null; + else if (_compareTo == null || !refs.Contains(_compareTo)) + autoSelected = refs[0]; + + Refs = refs; + CompareTo = autoSelected; + } + + private Repository _repo; + private object _basedOn = null; + private object _compareTo = null; + private List _refs = []; + private string _filter; + } +} diff --git a/src/ViewModels/Conflict.cs b/src/ViewModels/Conflict.cs index ccb8816d5..d98187d35 100644 --- a/src/ViewModels/Conflict.cs +++ b/src/ViewModels/Conflict.cs @@ -1,30 +1,11 @@ -using System; +using System.IO; using System.Threading.Tasks; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class ConflictSourceBranch - { - public string Name { get; private set; } - public string Head { get; private set; } - public Models.Commit Revision { get; private set; } - - public ConflictSourceBranch(string name, string head, Models.Commit revision) - { - Name = name; - Head = head; - Revision = revision; - } - - public ConflictSourceBranch(Repository repo, Models.Branch branch) - { - Name = branch.Name; - Head = branch.Head; - Revision = new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResult() ?? new Models.Commit() { SHA = branch.Head }; - } - } - - public class Conflict + public class Conflict : ObservableObject { public string Marker { @@ -36,70 +17,56 @@ public string Description get => _change.ConflictDesc; } - public object Theirs + public Models.ConflictFileState State { - get; - private set; + get => _state; + private set => SetProperty(ref _state, value); } - public object Mine + public object Theirs { - get; - private set; + get => _theirs; + private set => SetProperty(ref _theirs, value); } - public bool IsResolved - { - get; - private set; - } = false; - - public bool CanUseExternalMergeTool + public object Mine { - get; - private set; - } = false; + get => _mine; + private set => SetProperty(ref _mine, value); + } public Conflict(Repository repo, WorkingCopy wc, Models.Change change) { + _repo = repo; _wc = wc; _change = change; - var isSubmodule = repo.Submodules.Find(x => x.Path.Equals(change.Path, StringComparison.Ordinal)) != null; - if (!isSubmodule && (_change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified)) + Task.Run(async () => { - CanUseExternalMergeTool = true; - IsResolved = new Commands.IsConflictResolved(repo.FullPath, change).GetResult(); - } - - switch (wc.InProgressContext) - { - case CherryPickInProgress cherryPick: - Theirs = cherryPick.Head; - Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); - break; - case RebaseInProgress rebase: - var b = repo.Branches.Find(x => x.IsLocal && x.Name == rebase.HeadName); - if (b != null) - Theirs = new ConflictSourceBranch(b.Name, b.Head, rebase.StoppedAt); - else - Theirs = new ConflictSourceBranch(rebase.HeadName, rebase.StoppedAt?.SHA ?? "----------", rebase.StoppedAt); - - Mine = rebase.Onto; - break; - case RevertInProgress revert: - Theirs = revert.Head; - Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); - break; - case MergeInProgress merge: - Theirs = merge.Source; - Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); - break; - default: - Theirs = "Stash or Patch"; - Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); - break; - } + _head = new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResult(); + + var (mine, theirs) = wc.InProgressContext switch + { + CherryPickInProgress cherryPick => (_head, cherryPick.Head), + RebaseInProgress rebase => (rebase.Onto, rebase.StoppedAt), + RevertInProgress revert => (_head, revert.Head), + MergeInProgress merge => (_head, merge.Source), + _ => (_head, (object)"Stash or Patch"), + }; + + var state = Models.ConflictFileState.Unknown; + if ((_change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified) && !Directory.Exists(Path.Combine(_repo.FullPath, _change.Path))) + state = await new Commands.QueryConflictFileState(repo.FullPath, change) + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + State = state; + Mine = mine; + Theirs = theirs; + }); + }); } public async Task UseTheirsAsync() @@ -112,12 +79,23 @@ public async Task UseMineAsync() await _wc.UseMineAsync([_change]); } - public async Task OpenExternalMergeToolAsync() + public MergeConflictEditor CreateOpenMergeEditorRequest() + { + return _state == Models.ConflictFileState.UnmergedText ? new MergeConflictEditor(_repo, _head, _change.Path) : null; + } + + public async Task MergeExternalAsync() { - await _wc.UseExternalMergeToolAsync(_change); + if (_state == Models.ConflictFileState.UnmergedText) + await _wc.UseExternalMergeToolAsync(_change); } + private Repository _repo = null; private WorkingCopy _wc = null; private Models.Change _change = null; + private Models.Commit _head = null; + private Models.ConflictFileState _state = Models.ConflictFileState.Unknown; + private object _mine = null; + private object _theirs = null; } } diff --git a/src/ViewModels/ConventionalCommitMessageBuilder.cs b/src/ViewModels/ConventionalCommitMessageBuilder.cs index 31d7f438e..99c57d819 100644 --- a/src/ViewModels/ConventionalCommitMessageBuilder.cs +++ b/src/ViewModels/ConventionalCommitMessageBuilder.cs @@ -20,7 +20,11 @@ public List Types public Models.ConventionalCommitType SelectedType { get => _selectedType; - set => SetProperty(ref _selectedType, value, true); + set + { + if (SetProperty(ref _selectedType, value, true) && value is { PrefillShortDesc: { Length: > 0 } }) + Description = value.PrefillShortDesc; + } } public string Scope diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index 2fbb4a105..f180c94e9 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -20,7 +20,12 @@ public object BasedOn get; } - public bool DiscardLocalChanges + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + + public Models.DealWithLocalChanges DealWithLocalChanges { get; set; @@ -28,13 +33,14 @@ public bool DiscardLocalChanges public bool CheckoutAfterCreated { - get => _repo.Settings.CheckoutBranchOnCreateBranch; + get => _repo.UIStates.CheckoutBranchOnCreateBranch; set { - if (_repo.Settings.CheckoutBranchOnCreateBranch != value) + if (_repo.UIStates.CheckoutBranchOnCreateBranch != value) { - _repo.Settings.CheckoutBranchOnCreateBranch = value; + _repo.UIStates.CheckoutBranchOnCreateBranch = value; OnPropertyChanged(); + UpdateOverrideTip(); } } } @@ -44,6 +50,12 @@ public bool IsBareRepository get => _repo.IsBare; } + public string OverrideTip + { + get => _overrideTip; + private set => SetProperty(ref _overrideTip, value); + } + public bool AllowOverwrite { get => _allowOverwrite; @@ -54,45 +66,49 @@ 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; _baseOnRevision = branch.Head; + _committerDate = branch.CommitterDate; + _head = branch.Head; - if (!branch.IsLocal && repo.Branches.Find(x => x.IsLocal && x.Name == branch.Name) == null) + if (!branch.IsLocal) Name = branch.Name; BasedOn = branch; - DiscardLocalChanges = false; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; + UpdateOverrideTip(); } public CreateBranch(Repository repo, Models.Commit commit) { _repo = repo; _baseOnRevision = commit.SHA; + _committerDate = commit.CommitterTime; + _head = commit.SHA; BasedOn = commit; - DiscardLocalChanges = false; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; + UpdateOverrideTip(); } public CreateBranch(Repository repo, Models.Tag tag) { _repo = repo; _baseOnRevision = tag.SHA; + _committerDate = tag.CreatorDate; + _head = tag.SHA; BasedOn = tag; - DiscardLocalChanges = false; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; + UpdateOverrideTip(); } public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) @@ -136,42 +152,57 @@ public override async Task Sure() } } + Models.Branch created = new() + { + Name = _name, + FullName = $"refs/heads/{_name}", + CommitterDate = _committerDate, + Head = _head, + IsLocal = true, + }; + bool succ; if (CheckoutAfterCreated && !_repo.IsBare) { var needPopStash = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(_name, _baseOnRevision, false, _allowOverwrite); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) .Use(log) - .PushAsync("CREATE_BRANCH_AUTO_STASH"); + .PushAsync("CREATE_BRANCH_AUTO_STASH", false); if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .BranchAsync(_name, _baseOnRevision, DiscardLocalChanges, _allowOverwrite); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(_name, _baseOnRevision, false, _allowOverwrite); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(_name, _baseOnRevision, true, _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); - } + await _repo.AutoUpdateSubmodulesAsync(log); if (needPopStash) await new Commands.Stash(_repo.FullPath) @@ -186,43 +217,39 @@ public override async Task Sure() .CreateAsync(_baseOnRevision, _allowOverwrite); } - if (succ && BasedOn is Models.Branch { IsLocal: false } basedOn && _name.Equals(basedOn.Name, StringComparison.Ordinal)) + if (succ) { - await new Commands.Branch(_repo.FullPath, _name) + if (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/{_name}" }; - if (BasedOn is Models.Branch { IsLocal: false } based) - fake.Upstream = based.FullName; - - var folderEndIdx = fake.FullName.LastIndexOf('/'); - if (folderEndIdx > 10) - _repo.Settings.ExpandedBranchNodesInSideBar.Add(fake.FullName.Substring(0, folderEndIdx)); + created.Upstream = basedOn.FullName; + } - if (_repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(fake, Models.FilterMode.Included, false, false); + _repo.RefreshAfterCreateBranch(created, CheckoutAfterCreated); } - - _repo.MarkBranchesDirtyManually(); - - if (CheckoutAfterCreated) + else { - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); + _repo.MarkWorkingCopyDirtyManually(); } + log.Complete(); return true; } + private void UpdateOverrideTip() + { + OverrideTip = CheckoutAfterCreated ? "-B in `git checkout`" : "-f in `git branch`"; + } + private readonly Repository _repo = null; - private string _name = null; private readonly string _baseOnRevision = null; + private readonly ulong _committerDate = 0; + private readonly string _head = string.Empty; + private string _name = null; + private string _overrideTip = "-B"; private bool _allowOverwrite = false; } } diff --git a/src/ViewModels/CreateTag.cs b/src/ViewModels/CreateTag.cs index 4cf1fca30..ff729aab9 100644 --- a/src/ViewModels/CreateTag.cs +++ b/src/ViewModels/CreateTag.cs @@ -29,8 +29,15 @@ public string Message public bool Annotated { - get => _annotated; - set => SetProperty(ref _annotated, value); + get => _repo.UIStates.CreateAnnotatedTag; + set + { + if (_repo.UIStates.CreateAnnotatedTag != value) + { + _repo.UIStates.CreateAnnotatedTag = value; + OnPropertyChanged(); + } + } } public bool SignTag @@ -41,8 +48,8 @@ public bool SignTag public bool PushToRemotes { - get => _repo.Settings.PushToRemoteWhenCreateTag; - set => _repo.Settings.PushToRemoteWhenCreateTag = value; + get => _repo.UIStates.PushToRemoteWhenCreateTag; + set => _repo.UIStates.PushToRemoteWhenCreateTag = value; } public CreateTag(Repository repo, Models.Branch branch) @@ -83,9 +90,11 @@ public override async Task Sure() var log = _repo.CreateLog("Create Tag"); Use(log); - var cmd = new Commands.Tag(_repo.FullPath, _tagName).Use(log); - var succ = false; - if (_annotated) + var cmd = new Commands.Tag(_repo.FullPath, _tagName) + .Use(log); + + bool succ; + if (_repo.UIStates.CreateAnnotatedTag) succ = await cmd.AddAsync(_basedOn, Message, SignTag); else succ = await cmd.AddAsync(_basedOn); @@ -104,7 +113,6 @@ public override async Task Sure() private readonly Repository _repo = null; private string _tagName = string.Empty; - private bool _annotated = true; private readonly string _basedOn; } } diff --git a/src/ViewModels/DeinitSubmodule.cs b/src/ViewModels/DeinitSubmodule.cs index d224ce03e..f9fff198d 100644 --- a/src/ViewModels/DeinitSubmodule.cs +++ b/src/ViewModels/DeinitSubmodule.cs @@ -36,6 +36,7 @@ public override async Task Sure() .DeinitAsync(Submodule, false); log.Complete(); + _repo.MarkSubmodulesDirtyManually(); return succ; } diff --git a/src/ViewModels/DeleteBranch.cs b/src/ViewModels/DeleteBranch.cs index 6611194a2..ed8e8ff0b 100644 --- a/src/ViewModels/DeleteBranch.cs +++ b/src/ViewModels/DeleteBranch.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Text; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -9,34 +11,16 @@ public Models.Branch Target get; } - public Models.Branch TrackingRemoteBranch + public bool Force { get; - } - - public string DeleteTrackingRemoteTip - { - get; - private set; - } - - public bool AlsoDeleteTrackingRemote - { - get => _alsoDeleteTrackingRemote; - set => SetProperty(ref _alsoDeleteTrackingRemote, value); + set; } public DeleteBranch(Repository repo, Models.Branch branch) { _repo = repo; Target = branch; - - if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) - { - TrackingRemoteBranch = repo.Branches.Find(x => x.FullName == branch.Upstream); - if (TrackingRemoteBranch != null) - DeleteTrackingRemoteTip = App.Text("DeleteBranch.WithTrackingRemote", TrackingRemoteBranch.FriendlyName); - } } public override async Task Sure() @@ -47,49 +31,67 @@ public override async Task Sure() var log = _repo.CreateLog("Delete Branch"); Use(log); + var succ = false; if (Target.IsLocal) { - await new Commands.Branch(_repo.FullPath, Target.Name) + succ = await new Commands.Branch(_repo.FullPath, Target.Name) .Use(log) - .DeleteLocalAsync(); - _repo.HistoryFilterCollection.RemoveFilter(Target.FullName, Models.FilterType.LocalBranch); + .DeleteLocalAsync(Force); - if (_alsoDeleteTrackingRemote && TrackingRemoteBranch != null) + if (succ) { - await DeleteRemoteBranchAsync(TrackingRemoteBranch, log); - _repo.HistoryFilterCollection.RemoveFilter(TrackingRemoteBranch.FullName, Models.FilterType.RemoteBranch); + _repo.UIStates.RemoveHistoryFilter(Target.FullName, Models.FilterType.LocalBranch); + + var upstream = Target.Upstream ?? string.Empty; + var tracking = _repo.Branches.Find(x => x.FullName.Equals(upstream, StringComparison.Ordinal)); + if (tracking != null && tracking.Name.Equals(Target.Name, StringComparison.Ordinal)) + { + var msgBuilder = new StringBuilder(); + msgBuilder + .AppendLine(App.Text("DeleteBranch.AskForRemote")) + .AppendLine() + .Append("• ").Append(tracking.FriendlyName); + + var deleteTracking = await App.AskConfirmAsync(msgBuilder.ToString(), Models.ConfirmButtonType.YesNo); + if (deleteTracking) + { + succ = await DeleteRemoteBranchAsync(tracking, log); + if (succ) + _repo.UIStates.RemoveHistoryFilter(tracking.FullName, Models.FilterType.RemoteBranch); + } + } } } else { - await DeleteRemoteBranchAsync(Target, log); - _repo.HistoryFilterCollection.RemoveFilter(Target.FullName, Models.FilterType.RemoteBranch); + succ = await DeleteRemoteBranchAsync(Target, log); + if (succ) + _repo.UIStates.RemoveHistoryFilter(Target.FullName, Models.FilterType.RemoteBranch); } log.Complete(); _repo.MarkBranchesDirtyManually(); - return true; + return succ; } - private async Task DeleteRemoteBranchAsync(Models.Branch branch, CommandLog log) + private async Task DeleteRemoteBranchAsync(Models.Branch branch, CommandLog log) { var exists = await new Commands.Remote(_repo.FullPath) .HasBranchAsync(branch.Remote, branch.Name) .ConfigureAwait(false); if (exists) - await new Commands.Push(_repo.FullPath, branch.Remote, $"refs/heads/{branch.Name}", true) + return await new Commands.Push(_repo.FullPath, branch.Remote, $"refs/heads/{branch.Name}", true) .Use(log) .RunAsync() .ConfigureAwait(false); else - await new Commands.Branch(_repo.FullPath, branch.Name) + return await new Commands.Branch(_repo.FullPath, branch.Name) .Use(log) - .DeleteRemoteAsync(branch.Remote) + .DeleteRemoteAsync(branch.Remote, Force) .ConfigureAwait(false); } private readonly Repository _repo = null; - private bool _alsoDeleteTrackingRemote = false; } } diff --git a/src/ViewModels/DeleteMultipleBranches.cs b/src/ViewModels/DeleteMultipleBranches.cs index b5d762a64..eae8192dc 100644 --- a/src/ViewModels/DeleteMultipleBranches.cs +++ b/src/ViewModels/DeleteMultipleBranches.cs @@ -10,6 +10,12 @@ public List Targets get; } + public bool Force + { + get; + set; + } = false; + public DeleteMultipleBranches(Repository repo, List branches, bool isLocal) { _repo = repo; @@ -30,7 +36,7 @@ public override async Task Sure() foreach (var target in Targets) await new Commands.Branch(_repo.FullPath, target.Name) .Use(log) - .DeleteLocalAsync(); + .DeleteLocalAsync(Force); } else { @@ -44,7 +50,7 @@ public override async Task Sure() else await new Commands.Branch(_repo.FullPath, target.Name) .Use(log) - .DeleteRemoteAsync(target.Remote); + .DeleteRemoteAsync(target.Remote, Force); } } diff --git a/src/ViewModels/DeleteTag.cs b/src/ViewModels/DeleteTag.cs index 95790940c..6dc6ebb9c 100644 --- a/src/ViewModels/DeleteTag.cs +++ b/src/ViewModels/DeleteTag.cs @@ -12,8 +12,8 @@ public Models.Tag Target public bool PushToRemotes { - get => _repo.Settings.PushToRemoteWhenDeleteTag; - set => _repo.Settings.PushToRemoteWhenDeleteTag = value; + get => _repo.UIStates.PushToRemoteWhenDeleteTag; + set => _repo.UIStates.PushToRemoteWhenDeleteTag = value; } public DeleteTag(Repository repo, Models.Tag tag) @@ -44,7 +44,7 @@ public override async Task Sure() } log.Complete(); - _repo.HistoryFilterCollection.RemoveFilter(Target.Name, Models.FilterType.Tag); + _repo.UIStates.RemoveHistoryFilter(Target.Name, Models.FilterType.Tag); _repo.MarkTagsDirtyManually(); return succ; } diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index c24ee6cec..27eb93b8d 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -13,56 +13,16 @@ public string Title get; } - public bool IgnoreWhitespace + public int OldMode { - get => Preferences.Instance.IgnoreWhitespaceChangesInDiff; - set - { - if (value != Preferences.Instance.IgnoreWhitespaceChangesInDiff) - { - Preferences.Instance.IgnoreWhitespaceChangesInDiff = value; - OnPropertyChanged(); - 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(); - } - } + get => _oldMode; + private set => SetProperty(ref _oldMode, value); } - public string FileModeChange + public int NewMode { - get => _fileModeChange; - private set => SetProperty(ref _fileModeChange, value); + get => _newMode; + private set => SetProperty(ref _newMode, value); } public bool IsTextDiff @@ -71,6 +31,12 @@ public bool IsTextDiff private set => SetProperty(ref _isTextDiff, value); } + public bool IsIgnoreWhitespaceVisible + { + get => _isIgnoreWhitespaceVisible; + private set => SetProperty(ref _isIgnoreWhitespaceVisible, value); + } + public object Content { get => _content; @@ -91,8 +57,10 @@ public DiffContext(string repo, Models.DiffOption option, DiffContext previous = if (previous != null) { _isTextDiff = previous._isTextDiff; + _isIgnoreWhitespaceVisible = previous._isIgnoreWhitespaceVisible; _content = previous._content; - _fileModeChange = previous._fileModeChange; + _oldMode = previous._oldMode; + _newMode = previous._newMode; _unifiedLines = previous._unifiedLines; _info = previous._info; } @@ -124,36 +92,49 @@ public void OpenExternalMergeTool() public void CheckSettings() { + var pref = Preferences.Instance; + if (Content is TextDiffContext ctx) { - if ((ShowEntireFile && _info.UnifiedLines != _entireFileLine) || - (!ShowEntireFile && _info.UnifiedLines == _entireFileLine) || - (IgnoreWhitespace != _info.IgnoreWhitespace)) + if ((pref.UseFullTextDiff && _info.UnifiedLines != _entireFileLines) || + (!pref.UseFullTextDiff && _info.UnifiedLines == _entireFileLines) || + (pref.IgnoreWhitespaceChangesInDiff != _info.IgnoreWhitespace)) { LoadContent(); return; } - if (ctx.IsSideBySide() != UseSideBySide) + if (ctx.IsSideBySide() != pref.UseSideBySideDiff) Content = ctx.SwitchMode(); } + else if (Content is Models.NoOrEOLChange) + { + if (pref.IgnoreWhitespaceChangesInDiff != _info.IgnoreWhitespace) + LoadContent(); + } } private void LoadContent() { if (_option.Path.EndsWith('/')) { - Content = null; + OldMode = 0; + NewMode = 160000; IsTextDiff = false; + IsIgnoreWhitespaceVisible = false; + Content = null; + _info = null; return; } Task.Run(async () => { - var numLines = Preferences.Instance.UseFullTextDiff ? _entireFileLine : _unifiedLines; - var ignoreWhitespace = Preferences.Instance.IgnoreWhitespaceChangesInDiff; + var pref = Preferences.Instance; + var numLines = pref.UseFullTextDiff ? _entireFileLines : _unifiedLines; + var ignoreWhitespace = pref.IgnoreWhitespaceChangesInDiff; + var ignoreCRAtEOL = pref.IgnoreCRAtEOLInDiff; - var latest = await new Commands.Diff(_repo, _option, numLines, ignoreWhitespace) + var latest = await new Commands.Diff(_repo, _option, numLines, ignoreWhitespace, ignoreCRAtEOL) .ReadAsync() .ConfigureAwait(false); @@ -164,100 +145,33 @@ private void LoadContent() _info = info; object rs = null; - if (latest.TextDiff != null) + if (latest.TextDiff is { } textDiff) { - var count = latest.TextDiff.Lines.Count; - var isSubmodule = false; - if (count <= 3) - { - var submoduleDiff = new Models.SubmoduleDiff(); - var submoduleRoot = $"{_repo}/{_option.Path}".Replace('\\', '/').TrimEnd('/'); - isSubmodule = true; - for (int i = 1; i < count; i++) - { - var line = latest.TextDiff.Lines[i]; - if (!line.Content.StartsWith("Subproject commit ", StringComparison.Ordinal)) - { - isSubmodule = false; - break; - } - - var sha = line.Content.Substring(18); - if (line.Type == Models.TextDiffLineType.Added) - submoduleDiff.New = await QuerySubmoduleRevisionAsync(submoduleRoot, sha).ConfigureAwait(false); - else if (line.Type == Models.TextDiffLineType.Deleted) - submoduleDiff.Old = await QuerySubmoduleRevisionAsync(submoduleRoot, sha).ConfigureAwait(false); - } - - if (isSubmodule) - rs = submoduleDiff; - } - - if (!isSubmodule) - rs = latest.TextDiff; + rs = textDiff; } - else if (latest.IsBinary) + else if (latest.LFSDiff is { } lfs) { - var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath; var imgDecoder = ImageSource.GetDecoder(_option.Path); - if (imgDecoder != Models.ImageDecoder.None) - { - var imgDiff = new Models.ImageDiff(); - - if (_option.Revisions.Count == 2) - { - var oldImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[0], oldPath, imgDecoder).ConfigureAwait(false); - var newImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[1], _option.Path, imgDecoder).ConfigureAwait(false); - imgDiff.Old = oldImage.Bitmap; - imgDiff.OldFileSize = oldImage.Size; - imgDiff.New = newImage.Bitmap; - imgDiff.NewFileSize = newImage.Size; - } - else - { - if (!oldPath.Equals("/dev/null", StringComparison.Ordinal)) - { - var oldImage = await ImageSource.FromRevisionAsync(_repo, "HEAD", oldPath, imgDecoder).ConfigureAwait(false); - imgDiff.Old = oldImage.Bitmap; - imgDiff.OldFileSize = oldImage.Size; - } - - var fullPath = Path.Combine(_repo, _option.Path); - if (File.Exists(fullPath)) - { - var newImage = await ImageSource.FromFileAsync(fullPath, imgDecoder).ConfigureAwait(false); - imgDiff.New = newImage.Bitmap; - imgDiff.NewFileSize = newImage.Size; - } - } - - rs = imgDiff; - } + rs = new LFSImageDiff(_repo, lfs, imgDecoder); else - { - var binaryDiff = new Models.BinaryDiff(); - if (_option.Revisions.Count == 2) - { - binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, _option.Revisions[0]).GetResultAsync().ConfigureAwait(false); - binaryDiff.NewSize = await new Commands.QueryFileSize(_repo, _option.Path, _option.Revisions[1]).GetResultAsync().ConfigureAwait(false); - } - else - { - var fullPath = Path.Combine(_repo, _option.Path); - binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, "HEAD").GetResultAsync().ConfigureAwait(false); - binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; - } - rs = binaryDiff; - } + rs = lfs; } - else if (latest.IsLFS) + else if (latest.IsBinary) { var imgDecoder = ImageSource.GetDecoder(_option.Path); if (imgDecoder != Models.ImageDecoder.None) - rs = new LFSImageDiff(_repo, latest.LFSDiff, imgDecoder); + rs = await CreateImageDiffAsync(imgDecoder).ConfigureAwait(false); else - rs = latest.LFSDiff; + rs = await CreateBinaryDiffAsync().ConfigureAwait(false); + } + else if (latest.IsSubmoduleChange) + { + rs = await CreateSubmoduleDiffAsync(latest.OldHash, latest.NewHash).ConfigureAwait(false); + } + else if (IsEmptyFileHash(latest.OldHash) || IsEmptyFileHash(latest.NewHash)) + { + rs = new Models.EmptyFile(); } else { @@ -266,11 +180,13 @@ private void LoadContent() Dispatcher.UIThread.Post(() => { - FileModeChange = latest.FileModeChange; + OldMode = latest.OldMode; + NewMode = latest.NewMode; if (rs is Models.TextDiff cur) { IsTextDiff = true; + IsIgnoreWhitespaceVisible = true; if (Preferences.Instance.UseSideBySideDiff) Content = new TwoSideTextDiff(_option, cur, _content as TextDiffContext); @@ -280,24 +196,128 @@ private void LoadContent() else { IsTextDiff = false; + IsIgnoreWhitespaceVisible = (rs is Models.NoOrEOLChange); Content = rs; } }); }); } - private async Task QuerySubmoduleRevisionAsync(string repo, string sha) + private async Task CreateImageDiffAsync(Models.ImageDecoder imgDecoder) + { + var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath; + var imgDiff = new Models.ImageDiff(); + var fullPath = Path.Combine(_repo, _option.Path); + + if (_option.Revisions.Count == 2) + { + if (_option.Revisions[0].Equals("-R", StringComparison.Ordinal)) + { + var oldImage = await ImageSource.FromFileAsync(fullPath, imgDecoder).ConfigureAwait(false); + imgDiff.Old = oldImage.Bitmap; + imgDiff.OldFileSize = oldImage.Size; + } + else + { + var oldImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[0], oldPath, imgDecoder).ConfigureAwait(false); + imgDiff.Old = oldImage.Bitmap; + imgDiff.OldFileSize = oldImage.Size; + } + + var newImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[1], _option.Path, imgDecoder).ConfigureAwait(false); + imgDiff.New = newImage.Bitmap; + imgDiff.NewFileSize = newImage.Size; + } + else + { + if (!oldPath.Equals("/dev/null", StringComparison.Ordinal)) + { + var oldImage = await ImageSource.FromRevisionAsync(_repo, "HEAD", oldPath, imgDecoder).ConfigureAwait(false); + imgDiff.Old = oldImage.Bitmap; + imgDiff.OldFileSize = oldImage.Size; + } + + var newImage = await ImageSource.FromFileAsync(fullPath, imgDecoder).ConfigureAwait(false); + imgDiff.New = newImage.Bitmap; + imgDiff.NewFileSize = newImage.Size; + } + + return imgDiff; + } + + private async Task CreateBinaryDiffAsync() + { + var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath; + var binaryDiff = new Models.BinaryDiff(); + var fullPath = Path.Combine(_repo, _option.Path); + + if (_option.Revisions.Count == 2) + { + if (_option.Revisions[0].Equals("-R", StringComparison.Ordinal)) + { + binaryDiff.OldSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; + binaryDiff.NewSize = await new Commands.QueryFileSize(_repo, _option.Path, _option.Revisions[1]).GetResultAsync().ConfigureAwait(false); + } + else + { + binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, _option.Revisions[0]).GetResultAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(_option.Revisions[1])) + binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; + else + binaryDiff.NewSize = await new Commands.QueryFileSize(_repo, _option.Path, _option.Revisions[1]).GetResultAsync().ConfigureAwait(false); + } + } + else + { + binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, "HEAD").GetResultAsync().ConfigureAwait(false); + binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; + } + + return binaryDiff; + } + + private async Task CreateSubmoduleDiffAsync(string oldRevision, string newRevision) { - var commit = await new Commands.QuerySingleCommit(repo, sha).GetResultAsync().ConfigureAwait(false); - if (commit == null) - return new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = sha } }; + var submoduleDiff = new Models.SubmoduleDiff(); + var submoduleRoot = $"{_repo}/{_option.Path}".Replace('\\', '/').TrimEnd('/'); + submoduleDiff.FullPath = submoduleRoot; + + if (IsValidSubmoduleHash(oldRevision)) + submoduleDiff.Old = await new Commands.QuerySubmoduleRevision(submoduleRoot, oldRevision) + .GetResultAsync() + .ConfigureAwait(false); + + if (IsValidSubmoduleHash(newRevision)) + submoduleDiff.New = await new Commands.QuerySubmoduleRevision(submoduleRoot, newRevision) + .GetResultAsync() + .ConfigureAwait(false); + + return submoduleDiff; + } - var body = await new Commands.QueryCommitFullMessage(repo, sha).GetResultAsync().ConfigureAwait(false); - return new Models.RevisionSubmodule() + private bool IsValidSubmoduleHash(string hash) + { + for (int i = 0; i < hash.Length; i++) { - Commit = commit, - FullMessage = new Models.CommitFullMessage { Message = body } - }; + if (hash[i] != '0') + return true; + } + + return false; + } + + private bool IsEmptyFileHash(string hash) + { + if (string.IsNullOrEmpty(hash)) + return false; + + if (hash.Length == 40) + return hash.Equals(Models.EmptyFile.SHA1, StringComparison.Ordinal); + + if (hash.Length == 64) + return hash.Equals(Models.EmptyFile.SHA256, StringComparison.Ordinal); + + return false; } private class Info @@ -327,12 +347,14 @@ public bool IsSame(Info other) } } - private readonly int _entireFileLine = 999999999; + private readonly int _entireFileLines = 999999999; private readonly string _repo; private readonly Models.DiffOption _option = null; - private string _fileModeChange = string.Empty; + private int _oldMode = 0; + private int _newMode = 0; private int _unifiedLines = 4; private bool _isTextDiff = false; + private bool _isIgnoreWhitespaceVisible = true; private object _content = null; private Info _info = null; } diff --git a/src/ViewModels/DirHistories.cs b/src/ViewModels/DirHistories.cs index d4d37f25f..a1448009c 100644 --- a/src/ViewModels/DirHistories.cs +++ b/src/ViewModels/DirHistories.cs @@ -75,6 +75,39 @@ public DirHistories(Repository repo, string dir, string revision = null) }); } + public DirHistories(string repoPath, string dir) + { + Title = dir; + + var gitDir = new Commands.QueryGitDir(repoPath).GetResult(); + _repo = new Repository(true, repoPath, gitDir); // Trait repository as a bare repository to disable some file operations. + _repo.RefreshBranches(); + + _detail = new CommitDetail(_repo, null); + _detail.SearchChangeFilter = dir; + + Task.Run(async () => + { + var argsBuilder = new StringBuilder(); + argsBuilder + .Append("--date-order -n 10000 -- ") + .Append(dir.Quoted()); + + var commits = await new Commands.QueryCommits(_repo.FullPath, argsBuilder.ToString(), false) + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + Commits = commits; + IsLoading = false; + + if (commits.Count > 0) + SelectedCommit = commits[0]; + }); + }); + } + public void NavigateToCommit(Models.Commit commit) { _repo.NavigateToCommit(commit.SHA); diff --git a/src/ViewModels/Discard.cs b/src/ViewModels/Discard.cs index 99c225b9d..0a37b56ba 100644 --- a/src/ViewModels/Discard.cs +++ b/src/ViewModels/Discard.cs @@ -5,6 +5,12 @@ namespace SourceGit.ViewModels { public class DiscardAllMode { + public bool IncludeModified + { + get; + set; + } = true; + public bool IncludeUntracked { get; @@ -67,13 +73,18 @@ public override async Task Sure() 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); + { + await Commands.Discard.AllAsync(_repo.FullPath, all.IncludeModified, all.IncludeUntracked, all.IncludeIgnored, log); + _repo.ClearCommitMessage(); + } else + { await Commands.Discard.ChangesAsync(_repo.FullPath, _changes, log); + } log.Complete(); _repo.MarkWorkingCopyDirtyManually(); diff --git a/src/ViewModels/DropHead.cs b/src/ViewModels/DropHead.cs deleted file mode 100644 index b5baed06f..000000000 --- a/src/ViewModels/DropHead.cs +++ /dev/null @@ -1,63 +0,0 @@ -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/EditRepositoryNode.cs b/src/ViewModels/EditRepositoryNode.cs index d7176c786..40402ef67 100644 --- a/src/ViewModels/EditRepositoryNode.cs +++ b/src/ViewModels/EditRepositoryNode.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace SourceGit.ViewModels { public class EditRepositoryNode : Popup { - public string Id + public string Target { - get => _id; - set => SetProperty(ref _id, value); + get; + } + + public bool IsRepository + { + get; } [Required(ErrorMessage = "Name is required!")] @@ -19,34 +22,20 @@ public string Name set => SetProperty(ref _name, value, true); } - public List Bookmarks - { - get; - } - public int Bookmark { get => _bookmark; set => SetProperty(ref _bookmark, value); } - public bool IsRepository - { - get => _isRepository; - set => SetProperty(ref _isRepository, value); - } - public EditRepositoryNode(RepositoryNode node) { _node = node; - _id = node.Id; _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); + Target = node.IsRepository ? node.Id : node.Name; + IsRepository = node.IsRepository; } public override Task Sure() @@ -65,9 +54,7 @@ public override Task Sure() } private RepositoryNode _node = null; - private string _id = null; private string _name = null; - private bool _isRepository = false; private int _bookmark = 0; } } diff --git a/src/ViewModels/ExecuteCustomAction.cs b/src/ViewModels/ExecuteCustomAction.cs index 09026ee21..1c01fb820 100644 --- a/src/ViewModels/ExecuteCustomAction.cs +++ b/src/ViewModels/ExecuteCustomAction.cs @@ -18,15 +18,22 @@ public class CustomActionControlTextBox : ICustomActionControlParameter public string Label { get; set; } public string Placeholder { get; set; } public string Text { get; set; } + public string Formatter { get; set; } - public CustomActionControlTextBox(string label, string placeholder, string defaultValue) + public CustomActionControlTextBox(string label, string placeholder, string defaultValue, string formatter) { Label = label + ":"; Placeholder = placeholder; Text = defaultValue; + Formatter = formatter; } - public string GetValue() => Text; + public string GetValue() + { + if (string.IsNullOrEmpty(Text)) + return string.Empty; + return string.IsNullOrEmpty(Formatter) ? Text : Formatter.Replace("${VALUE}", Text); + } } public class CustomActionControlPathSelector : ObservableObject, ICustomActionControlParameter @@ -77,12 +84,7 @@ public class CustomActionControlComboBox : ObservableObject, ICustomActionContro public string Label { get; set; } public string Description { get; set; } public List Options { get; set; } = []; - - public string Value - { - get => _value; - set => SetProperty(ref _value, value); - } + public string Value { get; set; } public CustomActionControlComboBox(string label, string description, string options) { @@ -93,13 +95,69 @@ public CustomActionControlComboBox(string label, string description, string opti if (parts.Length > 0) { Options.AddRange(parts); - _value = parts[0]; + Value = parts[0]; + } + } + + public string GetValue() => Value; + } + + public class CustomActionControlBranchSelector : ObservableObject, ICustomActionControlParameter + { + public string Label { get; set; } + public string Description { get; set; } + public List Branches { get; set; } = []; + public Models.Branch SelectedBranch { get; set; } + + public CustomActionControlBranchSelector(string label, string description, Repository repo, bool isLocal, bool useFriendlyName, object target) + { + Label = label; + Description = description; + _useFriendlyName = useFriendlyName; + + foreach (var b in repo.Branches) + { + if (b.IsLocal == isLocal && !b.IsDetachedHead) + Branches.Add(b); } + + if (Branches.Count == 0) + return; + + if (target is Models.Branch branch) + { + if (isLocal) + { + var prefer = Branches.Find(b => b.FullName.Equals(branch.FullName, StringComparison.Ordinal)); + if (prefer != null) + { + SelectedBranch = prefer; + return; + } + } + else if (!string.IsNullOrEmpty(branch.Upstream)) + { + var upstream = Branches.Find(b => b.FullName.Equals(branch.Upstream, StringComparison.Ordinal)); + if (upstream != null) + { + SelectedBranch = upstream; + return; + } + } + } + + SelectedBranch = Branches[0]; } - public string GetValue() => _value; + public string GetValue() + { + if (SelectedBranch == null) + return string.Empty; - private string _value = string.Empty; + return _useFriendlyName ? SelectedBranch.FriendlyName : SelectedBranch.Name; + } + + private bool _useFriendlyName = false; } public class ExecuteCustomAction : Popup @@ -124,7 +182,31 @@ public ExecuteCustomAction(Repository repo, Models.CustomAction action, object s _repo = repo; CustomAction = action; Target = scopeTarget ?? new Models.Null(); - PrepareControlParameters(); + + foreach (var ctl in CustomAction.Controls) + { + switch (ctl.Type) + { + case Models.CustomActionControlType.TextBox: + ControlParameters.Add(new CustomActionControlTextBox(ctl.Label, ctl.Description, PrepareStringByTarget(ctl.StringValue), ctl.StringFormatter)); + break; + case Models.CustomActionControlType.PathSelector: + ControlParameters.Add(new CustomActionControlPathSelector(ctl.Label, ctl.Description, ctl.BoolValue, PrepareStringByTarget(ctl.StringValue))); + break; + case Models.CustomActionControlType.CheckBox: + ControlParameters.Add(new CustomActionControlCheckBox(ctl.Label, ctl.Description, ctl.StringValue, ctl.BoolValue)); + break; + case Models.CustomActionControlType.ComboBox: + ControlParameters.Add(new CustomActionControlComboBox(ctl.Label, ctl.Description, PrepareStringByTarget(ctl.StringValue))); + break; + case Models.CustomActionControlType.LocalBranchSelector: + ControlParameters.Add(new CustomActionControlBranchSelector(ctl.Label, ctl.Description, _repo, true, false, scopeTarget ?? _repo.CurrentBranch)); + break; + case Models.CustomActionControlType.RemoteBranchSelector: + ControlParameters.Add(new CustomActionControlBranchSelector(ctl.Label, ctl.Description, _repo, false, ctl.BoolValue, scopeTarget ?? _repo.CurrentBranch)); + break; + } + } } public override async Task Sure() @@ -153,48 +235,29 @@ public override async Task Sure() return true; } - private void PrepareControlParameters() - { - foreach (var ctl in CustomAction.Controls) - { - switch (ctl.Type) - { - case Models.CustomActionControlType.TextBox: - ControlParameters.Add(new CustomActionControlTextBox(ctl.Label, ctl.Description, PrepareStringByTarget(ctl.StringValue))); - break; - case Models.CustomActionControlType.PathSelector: - ControlParameters.Add(new CustomActionControlPathSelector(ctl.Label, ctl.Description, ctl.BoolValue, PrepareStringByTarget(ctl.StringValue))); - break; - case Models.CustomActionControlType.CheckBox: - ControlParameters.Add(new CustomActionControlCheckBox(ctl.Label, ctl.Description, ctl.StringValue, ctl.BoolValue)); - break; - case Models.CustomActionControlType.ComboBox: - ControlParameters.Add(new CustomActionControlComboBox(ctl.Label, ctl.Description, PrepareStringByTarget(ctl.StringValue))); - break; - } - } - } - private string PrepareStringByTarget(string org) { - org = org.Replace("${REPO}", GetWorkdir()); + var repoPath = OperatingSystem.IsWindows() ? _repo.FullPath.Replace("/", "\\") : _repo.FullPath; + org = org.Replace("${REPO}", repoPath); + + if (Target is Models.Branch { IsDetachedHead: false } targetBranch) + org = org.Replace("${BRANCH}", targetBranch.Name).Replace("${BRANCH_FRIENDLY_NAME}", targetBranch.FriendlyName).Replace("${REMOTE}", targetBranch.Remote); + else if (_repo.CurrentBranch is { IsDetachedHead: false } currentBranch) + org = org.Replace("${BRANCH}", currentBranch.Name).Replace("${BRANCH_FRIENDLY_NAME}", currentBranch.FriendlyName); + else + org = org.Replace("${BRANCH}", "HEAD").Replace("${BRANCH_FRIENDLY_NAME}", "HEAD"); return Target switch { - Models.Branch b => org.Replace("${BRANCH_FRIENDLY_NAME}", b.FriendlyName).Replace("${BRANCH}", b.Name).Replace("${REMOTE}", b.Remote), + Models.Branch b => org.Replace("${SHA}", b.Head), Models.Commit c => org.Replace("${SHA}", c.SHA), - Models.Tag t => org.Replace("${TAG}", t.Name), + Models.Tag t => org.Replace("${SHA}", t.SHA).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), + Models.CustomActionTargetFile f => org.Replace("${SHA}", f.Revision?.SHA ?? "HEAD").Replace("${FILE}", f.File), _ => org }; } - private string GetWorkdir() - { - return OperatingSystem.IsWindows() ? _repo.FullPath.Replace("/", "\\") : _repo.FullPath; - } - private void Run(string args) { var start = new ProcessStartInfo(); @@ -210,7 +273,7 @@ private void Run(string args) } catch (Exception e) { - App.RaiseException(_repo.FullPath, e.Message); + _repo.SendNotification(e.Message, true); } } @@ -258,12 +321,12 @@ private async Task RunAsync(string args, Models.ICommandLog log) { var errMsg = builder.ToString().Trim(); if (!string.IsNullOrEmpty(errMsg)) - App.RaiseException(_repo.FullPath, errMsg); + _repo.SendNotification(errMsg, true); } } catch (Exception e) { - App.RaiseException(_repo.FullPath, e.Message); + _repo.SendNotification(e.Message, true); } } diff --git a/src/ViewModels/ExecuteCustomActionCommandPalette.cs b/src/ViewModels/ExecuteCustomActionCommandPalette.cs new file mode 100644 index 000000000..78290086a --- /dev/null +++ b/src/ViewModels/ExecuteCustomActionCommandPalette.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class ExecuteCustomActionCommandPaletteCmd + { + public Models.CustomAction Action { get; set; } + public bool IsGlobal { get; set; } + public string Name { get => Action.Name; } + + public ExecuteCustomActionCommandPaletteCmd(Models.CustomAction action, bool isGlobal) + { + Action = action; + IsGlobal = isGlobal; + } + } + + public class ExecuteCustomActionCommandPalette : ICommandPalette + { + public List VisibleActions + { + get => _visibleActions; + private set => SetProperty(ref _visibleActions, value); + } + + public ExecuteCustomActionCommandPaletteCmd Selected + { + get => _selected; + set => SetProperty(ref _selected, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateVisibleActions(); + } + } + + public ExecuteCustomActionCommandPalette(Repository repo) + { + _repo = repo; + + var actions = repo.GetCustomActions(Models.CustomActionScope.Repository); + foreach (var (action, menu) in actions) + _actions.Add(new(action, menu.IsGlobal)); + + if (_actions.Count > 0) + { + _actions.Sort((l, r) => + { + if (l.IsGlobal != r.IsGlobal) + return l.IsGlobal ? -1 : 1; + + return l.Name.CompareTo(r.Name, StringComparison.OrdinalIgnoreCase); + }); + + _visibleActions = _actions; + _selected = _actions[0]; + } + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public async Task ExecAsync() + { + _actions.Clear(); + _visibleActions.Clear(); + Close(); + + if (_selected != null) + await _repo.ExecCustomActionAsync(_selected.Action, null); + } + + private void UpdateVisibleActions() + { + var filter = _filter?.Trim(); + if (string.IsNullOrEmpty(filter)) + { + VisibleActions = _actions; + return; + } + + var visible = new List(); + foreach (var act in _actions) + { + if (act.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)) + visible.Add(act); + } + + var autoSelected = _selected; + if (visible.Count == 0) + autoSelected = null; + else if (_selected == null || !visible.Contains(_selected)) + autoSelected = visible[0]; + + VisibleActions = visible; + Selected = autoSelected; + } + + private Repository _repo; + private List _actions = []; + private List _visibleActions = []; + private ExecuteCustomActionCommandPaletteCmd _selected = null; + private string _filter; + } +} diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 43bc5a18a..3236a6421 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -21,7 +21,7 @@ public bool FetchAllRemotes set { if (SetProperty(ref _fetchAllRemotes, value) && IsFetchAllRemoteVisible) - _repo.Settings.FetchAllRemotes = value; + _repo.UIStates.FetchAllRemotes = value; } } @@ -33,21 +33,21 @@ public Models.Remote SelectedRemote public bool NoTags { - get => _repo.Settings.FetchWithoutTags; - set => _repo.Settings.FetchWithoutTags = value; + get => _repo.UIStates.FetchWithoutTags; + set => _repo.UIStates.FetchWithoutTags = value; } public bool Force { - get => _repo.Settings.EnableForceOnFetch; - set => _repo.Settings.EnableForceOnFetch = value; + get => _repo.UIStates.EnableForceOnFetch; + set => _repo.UIStates.EnableForceOnFetch = value; } public Fetch(Repository repo, Models.Remote preferredRemote = null) { _repo = repo; IsFetchAllRemoteVisible = repo.Remotes.Count > 1 && preferredRemote == null; - _fetchAllRemotes = IsFetchAllRemoteVisible && _repo.Settings.FetchAllRemotes; + _fetchAllRemotes = IsFetchAllRemoteVisible && _repo.UIStates.FetchAllRemotes; if (preferredRemote != null) { @@ -68,9 +68,12 @@ public override async Task Sure() { 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 navigateToUpstreamHEAD = _repo.IsHistoriesVisible && + _repo.Histories.SelectedCommits.Count == 1 && + _repo.Histories.SelectedCommits[0].IsCurrentHead; + + var notags = _repo.UIStates.FetchWithoutTags; + var force = _repo.UIStates.EnableForceOnFetch; var log = _repo.CreateLog("Fetch"); Use(log); diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 91cd56236..374396f1f 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading.Tasks; - -using Avalonia.Collections; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -18,15 +14,27 @@ public class FileHistoriesRevisionFile(string path, object content = null, bool public bool CanOpenWithDefaultEditor { get; set; } = canOpenWithDefaultEditor; } + public class FileHistoriesSingleRevisionViewMode + { + public bool IsDiff + { + get; + set; + } = true; + } + public class FileHistoriesSingleRevision : ObservableObject { public bool IsDiffMode { - get => _isDiffMode; + get => _viewMode.IsDiff; set { - if (SetProperty(ref _isDiffMode, value)) + if (_viewMode.IsDiff != value) + { + _viewMode.IsDiff = value; RefreshViewContent(); + } } } @@ -36,17 +44,24 @@ public object ViewContent set => SetProperty(ref _viewContent, value); } - public FileHistoriesSingleRevision(string repo, string file, Models.Commit revision, bool prevIsDiffMode) + public FileHistoriesSingleRevision(string repo, Models.FileVersion revision, FileHistoriesSingleRevisionViewMode viewMode) { _repo = repo; - _file = file; + _file = revision.Path; _revision = revision; - _isDiffMode = prevIsDiffMode; + _viewMode = viewMode; _viewContent = null; RefreshViewContent(); } + public void SetRevision(Models.FileVersion revision) + { + _file = revision.Path; + _revision = revision; + RefreshViewContent(); + } + public async Task ResetToSelectedRevisionAsync() { return await new Commands.Checkout(_repo) @@ -73,9 +88,9 @@ await Commands.SaveRevisionFile private void RefreshViewContent() { - if (_isDiffMode) + if (_viewMode.IsDiff) { - SetViewContentAsDiff(); + ViewContent = new DiffContext(_repo, new(_revision), _viewContent as DiffContext); return; } @@ -138,14 +153,16 @@ private async Task GetRevisionFileContentAsync(Models.Object obj) if (obj.Type == Models.ObjectType.Commit) { - 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() + var submoduleRoot = Path.Combine(_repo, _file).Replace('\\', '/').TrimEnd('/'); + var module = await new Commands.QuerySubmoduleRevision(submoduleRoot, obj.SHA).GetResultAsync().ConfigureAwait(false); + if (module == null) { - Commit = commit ?? new Models.Commit() { SHA = obj.SHA }, - FullMessage = new Models.CommitFullMessage { Message = message } - }; + module = new Models.RevisionSubmodule() + { + Commit = new Models.Commit() { SHA = obj.SHA }, + FullMessage = new Models.CommitFullMessage { Message = null } + }; + } return new FileHistoriesRevisionFile(_file, module); } @@ -153,28 +170,22 @@ private async Task GetRevisionFileContentAsync(Models.Object obj) return new FileHistoriesRevisionFile(_file); } - private void SetViewContentAsDiff() - { - var option = new Models.DiffOption(_revision, _file); - ViewContent = new DiffContext(_repo, option, _viewContent as DiffContext); - } - private string _repo = null; private string _file = null; - private Models.Commit _revision = null; - private bool _isDiffMode = false; + private Models.FileVersion _revision = null; + private FileHistoriesSingleRevisionViewMode _viewMode = null; private object _viewContent = null; } public class FileHistoriesCompareRevisions : ObservableObject { - public Models.Commit StartPoint + public Models.FileVersion StartPoint { get => _startPoint; set => SetProperty(ref _startPoint, value); } - public Models.Commit EndPoint + public Models.FileVersion EndPoint { get => _endPoint; set => SetProperty(ref _endPoint, value); @@ -186,19 +197,18 @@ public DiffContext ViewContent set => SetProperty(ref _viewContent, value); } - public FileHistoriesCompareRevisions(string repo, string file, Models.Commit start, Models.Commit end) + public FileHistoriesCompareRevisions(string repo, Models.FileVersion start, Models.FileVersion end) { _repo = repo; - _file = file; _startPoint = start; _endPoint = end; - RefreshViewContent(); + _viewContent = new(_repo, new(start, end)); } public void Swap() { (StartPoint, EndPoint) = (_endPoint, _startPoint); - RefreshViewContent(); + ViewContent = new(_repo, new(_startPoint, _endPoint), _viewContent); } public async Task SaveAsPatch(string saveTo) @@ -208,27 +218,9 @@ public async Task SaveAsPatch(string saveTo) .ConfigureAwait(false); } - private void RefreshViewContent() - { - Task.Run(async () => - { - _changes = await new Commands.CompareRevisions(_repo, _startPoint.SHA, _endPoint.SHA, _file).ReadAsync().ConfigureAwait(false); - if (_changes.Count == 0) - { - Dispatcher.UIThread.Post(() => ViewContent = null); - } - else - { - var option = new Models.DiffOption(_startPoint.SHA, _endPoint.SHA, _changes[0]); - Dispatcher.UIThread.Post(() => ViewContent = new DiffContext(_repo, option, _viewContent)); - } - }); - } - private string _repo = null; - private string _file = null; - private Models.Commit _startPoint = null; - private Models.Commit _endPoint = null; + private Models.FileVersion _startPoint = null; + private Models.FileVersion _endPoint = null; private List _changes = []; private DiffContext _viewContent = null; } @@ -246,17 +238,21 @@ public bool IsLoading private set => SetProperty(ref _isLoading, value); } - public List Commits + public List Revisions { - get => _commits; - set => SetProperty(ref _commits, value); + get => _revisions; + set => SetProperty(ref _revisions, value); } - public AvaloniaList SelectedCommits + public List SelectedRevisions { - get; - set; - } = []; + get => _selectedRevisions; + set + { + if (SetProperty(ref _selectedRevisions, value)) + RefreshViewContent(); + } + } public object ViewContent { @@ -275,41 +271,19 @@ public FileHistories(string repo, string file, string commit = null) Task.Run(async () => { - var argsBuilder = new StringBuilder(); - argsBuilder - .Append("--date-order -n 10000 ") - .Append(commit ?? string.Empty) - .Append(" -- ") - .Append(file.Quoted()); - - var commits = await new Commands.QueryCommits(_repo, argsBuilder.ToString(), false) + var revisions = await new Commands.QueryFileHistory(_repo, file, commit) .GetResultAsync() .ConfigureAwait(false); Dispatcher.UIThread.Post(() => { IsLoading = false; - Commits = commits; - if (Commits.Count > 0) - SelectedCommits.Add(Commits[0]); + Revisions = revisions; }); }); - - SelectedCommits.CollectionChanged += (_, _) => - { - if (_viewContent is FileHistoriesSingleRevision singleRevision) - _prevIsDiffMode = singleRevision.IsDiffMode; - - ViewContent = SelectedCommits.Count switch - { - 1 => new FileHistoriesSingleRevision(_repo, file, SelectedCommits[0], _prevIsDiffMode), - 2 => new FileHistoriesCompareRevisions(_repo, file, SelectedCommits[0], SelectedCommits[1]), - _ => SelectedCommits.Count, - }; - }; } - public void NavigateToCommit(Models.Commit commit) + public void NavigateToCommit(Models.FileVersion revision) { var launcher = App.GetLauncher(); if (launcher != null) @@ -318,16 +292,16 @@ public void NavigateToCommit(Models.Commit commit) { if (page.Data is Repository repo && repo.FullPath.Equals(_repo, StringComparison.Ordinal)) { - repo.NavigateToCommit(commit.SHA); + repo.NavigateToCommit(revision.SHA); break; } } } } - public string GetCommitFullMessage(Models.Commit commit) + public string GetCommitFullMessage(Models.FileVersion revision) { - var sha = commit.SHA; + var sha = revision.SHA; if (_fullCommitMessages.TryGetValue(sha, out var msg)) return msg; @@ -336,10 +310,35 @@ public string GetCommitFullMessage(Models.Commit commit) return msg; } + private void RefreshViewContent() + { + var count = _selectedRevisions?.Count ?? 0; + if (count == 0) + { + ViewContent = null; + } + else if (count == 1) + { + if (_viewContent is FileHistoriesSingleRevision single) + single.SetRevision(_selectedRevisions[0]); + else + ViewContent = new FileHistoriesSingleRevision(_repo, _selectedRevisions[0], _viewMode); + } + else if (count == 2) + { + ViewContent = new FileHistoriesCompareRevisions(_repo, _selectedRevisions[0], _selectedRevisions[1]); + } + else + { + ViewContent = _selectedRevisions.Count; + } + } + private readonly string _repo = null; private bool _isLoading = true; - private bool _prevIsDiffMode = true; - private List _commits = null; + private FileHistoriesSingleRevisionViewMode _viewMode = new(); + private List _revisions = null; + private List _selectedRevisions = []; private Dictionary _fullCommitMessages = new(); private object _viewContent = null; } diff --git a/src/ViewModels/FileHistoryCommandPalette.cs b/src/ViewModels/FileHistoryCommandPalette.cs index fa2438163..097e85669 100644 --- a/src/ViewModels/FileHistoryCommandPalette.cs +++ b/src/ViewModels/FileHistoryCommandPalette.cs @@ -35,9 +35,8 @@ public string SelectedFile set => SetProperty(ref _selectedFile, value); } - public FileHistoryCommandPalette(Launcher launcher, string repo) + public FileHistoryCommandPalette(string repo) { - _launcher = launcher; _repo = repo; _isLoading = true; @@ -56,26 +55,17 @@ public FileHistoryCommandPalette(Launcher launcher, string repo) }); } - 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() + public FileHistories Launch() { - if (!string.IsNullOrEmpty(_selectedFile)) - App.ShowWindow(new FileHistories(_repo, _selectedFile)); - _launcher.CancelCommandPalette(); + _repoFiles.Clear(); + _visibleFiles.Clear(); + Close(); + return !string.IsNullOrEmpty(_selectedFile) ? new FileHistories(_repo, _selectedFile) : null; } private void UpdateVisible() @@ -111,7 +101,6 @@ private void UpdateVisible() } } - private Launcher _launcher = null; private string _repo = null; private bool _isLoading = false; private List _repoFiles = null; diff --git a/src/ViewModels/FilterModeInGraph.cs b/src/ViewModels/FilterModeInGraph.cs index 26d6edd13..773f4bc5b 100644 --- a/src/ViewModels/FilterModeInGraph.cs +++ b/src/ViewModels/FilterModeInGraph.cs @@ -22,9 +22,9 @@ public FilterModeInGraph(Repository repo, object target) _target = target; if (_target is Models.Branch b) - _mode = _repo.HistoryFilterCollection.GetFilterMode(b.FullName); + _mode = _repo.UIStates.GetHistoryFilterMode(b.FullName); else if (_target is Models.Tag t) - _mode = _repo.HistoryFilterCollection.GetFilterMode(t.Name); + _mode = _repo.UIStates.GetHistoryFilterMode(t.Name); } private void SetFilterMode(Models.FilterMode mode) diff --git a/src/ViewModels/GitFlowFinish.cs b/src/ViewModels/GitFlowFinish.cs index 7ed48cbf7..373e5398b 100644 --- a/src/ViewModels/GitFlowFinish.cs +++ b/src/ViewModels/GitFlowFinish.cs @@ -15,13 +15,13 @@ public Models.GitFlowBranchType Type private set; } - public bool Squash + public bool RebaseBeforeMerging { get; set; } = false; - public bool AutoPush + public bool Squash { get; set; @@ -50,7 +50,9 @@ public override async Task Sure() var prefix = _repo.GitFlow.GetPrefix(Type); var name = Branch.Name.StartsWith(prefix) ? Branch.Name.Substring(prefix.Length) : Branch.Name; - var succ = await Commands.GitFlow.FinishAsync(_repo.FullPath, Type, name, Squash, AutoPush, KeepBranch, log); + var succ = await new Commands.GitFlow(_repo.FullPath) + .Use(log) + .FinishAsync(Type, name, RebaseBeforeMerging, Squash, KeepBranch); log.Complete(); return succ; diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index ee8673aa3..2fe9c0784 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -1,4 +1,6 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -11,6 +13,18 @@ public Models.GitFlowBranchType Type private set; } + public List LocalBranches + { + get; + private set; + } + + public Models.Branch StartPoint + { + get => _startPoint; + set => SetProperty(ref _startPoint, value); + } + public string Prefix { get; @@ -32,6 +46,21 @@ public GitFlowStart(Repository repo, Models.GitFlowBranchType type) Type = type; Prefix = _repo.GitFlow.GetPrefix(type); + LocalBranches = new List(); + + foreach (var b in _repo.Branches) + { + if (b.IsLocal && !b.IsDetachedHead) + LocalBranches.Add(b); + } + + var defBranch = type switch + { + Models.GitFlowBranchType.Hotfix => _repo.GitFlow.ProductionBranch, + _ => _repo.GitFlow.DevelopmentBranch, + }; + + StartPoint = LocalBranches.Find(b => b.Name.Equals(defBranch, StringComparison.Ordinal)); } public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) @@ -57,12 +86,16 @@ public override async Task Sure() var log = _repo.CreateLog("GitFlow - Start"); Use(log); - var succ = await Commands.GitFlow.StartAsync(_repo.FullPath, Type, _name, log); + var succ = await new Commands.GitFlow(_repo.FullPath) + .Use(log) + .StartAsync(Type, _name, _startPoint); + log.Complete(); return succ; } private readonly Repository _repo; private string _name = null; + private Models.Branch _startPoint = null; } } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 455ce43ba..15cf15748 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,16 +1,16 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class Histories : ObservableObject, IDisposable + public class Histories : ObservableObject { public bool IsLoading { @@ -18,17 +18,66 @@ public bool IsLoading set => SetProperty(ref _isLoading, value); } + public bool IsAuthorColumnVisible + { + get => _repo.UIStates.IsAuthorColumnVisibleInHistory; + set + { + if (_repo.UIStates.IsAuthorColumnVisibleInHistory != value) + { + _repo.UIStates.IsAuthorColumnVisibleInHistory = value; + OnPropertyChanged(); + } + } + } + + public bool IsSHAColumnVisible + { + get => _repo.UIStates.IsSHAColumnVisibleInHistory; + set + { + if (_repo.UIStates.IsSHAColumnVisibleInHistory != value) + { + _repo.UIStates.IsSHAColumnVisibleInHistory = value; + OnPropertyChanged(); + } + } + } + + public bool IsAuthorTimeColumnVisible + { + get => _repo.UIStates.IsAuthorTimeColumnVisibleInHistory; + set + { + if (_repo.UIStates.IsAuthorTimeColumnVisibleInHistory != value) + { + _repo.UIStates.IsAuthorTimeColumnVisibleInHistory = value; + OnPropertyChanged(); + } + } + } + + public bool IsCommitTimeColumnVisible + { + get => _repo.UIStates.IsCommitTimeColumnVisibleInHistory; + set + { + if (_repo.UIStates.IsCommitTimeColumnVisibleInHistory != value) + { + _repo.UIStates.IsCommitTimeColumnVisibleInHistory = value; + OnPropertyChanged(); + } + } + } + public List Commits { get => _commits; set { - var lastSelected = AutoSelectedCommit; + GenerateGraph(value, true); if (SetProperty(ref _commits, value)) - { - if (value.Count > 0 && lastSelected != null) - AutoSelectedCommit = value.Find(x => x.SHA == lastSelected.SHA); - } + PostCommitsChanged(); } } @@ -38,22 +87,38 @@ public Models.CommitGraph Graph set => SetProperty(ref _graph, value); } - public Models.Commit AutoSelectedCommit + public Models.CommitGraphHighlighting GraphHighlighting { - get => _autoSelectedCommit; - set => SetProperty(ref _autoSelectedCommit, value); + get => _repo.UIStates.GraphHighlighting; + set + { + if (_repo.UIStates.GraphHighlighting != value) + { + _repo.UIStates.GraphHighlighting = value; + GenerateGraph(_commits); + } + } } - public long NavigationId + public List SelectedCommits { - get => _navigationId; - private set => SetProperty(ref _navigationId, value); + get => _selectedCommits; + set + { + var oldCount = _selectedCommits.Count; + if (SetProperty(ref _selectedCommits, value) && oldCount + value.Count > 0) + PostSelectedCommitsChanged(); + } } - public IDisposable DetailContext + public object DetailContext { get => _detailContext; - set => SetProperty(ref _detailContext, value); + set + { + if (SetProperty(ref _detailContext, value)) + OnPropertyChanged(nameof(IsOpenAsStandaloneVisible)); + } } public Models.Bisect Bisect @@ -62,6 +127,16 @@ public Models.Bisect Bisect private set => SetProperty(ref _bisect, value); } + public Models.Branch CurrentBranch + { + get => _repo.CurrentBranch; + } + + public AvaloniaList IssueTrackers + { + get => _repo.IssueTrackers; + } + public GridLength LeftArea { get => _leftArea; @@ -82,8 +157,36 @@ public GridLength TopArea public GridLength BottomArea { - get => _bottomArea; - set => SetProperty(ref _bottomArea, value); + get => _isCollapseDetails ? new GridLength(28, GridUnitType.Pixel) : _bottomArea; + set + { + if (!Preferences.Instance.UseTwoColumnsLayoutInHistories && !_isCollapseDetails) + SetProperty(ref _bottomArea, value); + } + } + + public double AuthorColumnWidth + { + get => _repo.UIStates.AuthorColumnWidth; + set => _repo.UIStates.AuthorColumnWidth = value; + } + + public bool IsOpenAsStandaloneVisible + { + get => DetailContext is CommitDetail or RevisionCompare; + } + + public bool IsCollapseDetails + { + get => _isCollapseDetails; + set + { + if (!Preferences.Instance.UseTwoColumnsLayoutInHistories && SetProperty(ref _isCollapseDetails, value)) + { + OnPropertyChanged(nameof(TopArea)); + OnPropertyChanged(nameof(BottomArea)); + } + } } public Histories(Repository repo) @@ -92,14 +195,9 @@ public Histories(Repository repo) _commitDetailSharedData = new CommitDetailSharedData(); } - public void Dispose() + public void NotifyCurrentBranchChanged() { - Commits = []; - _repo = null; - _graph = null; - _autoSelectedCommit = null; - _detailContext?.Dispose(); - _detailContext = null; + OnPropertyChanged(nameof(CurrentBranch)); } public Models.BisectState UpdateBisectInfo() @@ -111,26 +209,40 @@ public Models.BisectState UpdateBisectInfo() return Models.BisectState.None; } + var head = new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResult(); var info = new Models.Bisect(); + var markedHead = false; var dir = Path.Combine(_repo.GitDir, "refs", "bisect"); if (Directory.Exists(dir)) { var files = new DirectoryInfo(dir).GetFiles(); foreach (var file in files) { + var sha = File.ReadAllText(file.FullName).Trim(); + if (!markedHead) + markedHead = head.Equals(sha, StringComparison.Ordinal); + if (file.Name.StartsWith("bad")) - info.Bads.Add(File.ReadAllText(file.FullName).Trim()); + info.Bads.Add(sha); else if (file.Name.StartsWith("good")) - info.Goods.Add(File.ReadAllText(file.FullName).Trim()); + info.Goods.Add(sha); + else if (file.Name.StartsWith("skip")) + info.Skipped.Add(sha); } } Bisect = info; - if (info.Bads.Count == 0 || info.Goods.Count == 0) - return Models.BisectState.WaitingForRange; - else - return Models.BisectState.Detecting; + if (info.Bads.Count == 0) + return Models.BisectState.WaitingForFirstBad; + + if (markedHead) + return Models.BisectState.WaitingForCheckoutAnother; + + if (info.Goods.Count == 0) + return Models.BisectState.WaitingForFirstGood; + + return Models.BisectState.WaitingForMark; } public void NavigateTo(string commitSHA) @@ -138,7 +250,7 @@ public void NavigateTo(string commitSHA) var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal)); if (commit != null) { - NavigateTo(commit); + SelectedCommits = [commit]; return; } @@ -148,50 +260,38 @@ public void NavigateTo(string commitSHA) .GetResultAsync() .ConfigureAwait(false); - Dispatcher.UIThread.Post(() => NavigateTo(c)); + Dispatcher.UIThread.Post(() => + { + _ignoreSelectionChange = true; + SelectedCommits = []; + + if (_detailContext is CommitDetail detail) + { + detail.Commit = c; + } + else + { + var commitDetail = new CommitDetail(_repo, _commitDetailSharedData); + commitDetail.Commit = c; + DetailContext = commitDetail; + } + + _ignoreSelectionChange = false; + }); }); } - public void Select(IList commits) + public async Task GetCommitAsync(string sha) { - if (commits.Count == 0) - { - _repo.SearchCommitContext.Selected = null; - DetailContext = null; - } - else if (commits.Count == 1) - { - var commit = (commits[0] as Models.Commit)!; - 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; - - if (_detailContext is CommitDetail detail) - { - detail.Commit = commit; - } - else - { - var commitDetail = new CommitDetail(_repo, _commitDetailSharedData); - commitDetail.Commit = commit; - DetailContext = commitDetail; - } - } - else if (commits.Count == 2) - { - _repo.SearchCommitContext.Selected = null; + return await new Commands.QuerySingleCommit(_repo.FullPath, sha) + .GetResultAsync() + .ConfigureAwait(false); + } - var end = commits[0] as Models.Commit; - var start = commits[1] as Models.Commit; - DetailContext = new RevisionCompare(_repo.FullPath, start, end); - } - else - { - _repo.SearchCommitContext.Selected = null; - DetailContext = new Models.Count(commits.Count); - } + public void CheckoutCommitDetached(Models.Commit c) + { + if (!c.IsCurrentHead && _repo.CanCreatePopup()) + _repo.ShowPopup(new CheckoutDetached(_repo, c)); } public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) @@ -282,7 +382,7 @@ public async Task CheckoutBranchByCommitAsync(Models.Commit commit) if (firstRemoteBranch != null) _repo.ShowPopup(new CreateBranch(_repo, firstRemoteBranch)); else if (!_repo.IsBare) - _repo.ShowPopup(new CheckoutCommit(_repo, commit)); + _repo.ShowPopup(new CheckoutDetached(_repo, commit)); } } @@ -299,7 +399,7 @@ public async Task CherryPickAsync(Models.Commit commit) var parents = new List(); foreach (var sha in commit.Parents) { - var parent = _commits.Find(x => x.SHA == sha); + var parent = _commits.Find(x => x.SHA.Equals(sha, StringComparison.Ordinal)); if (parent == null) parent = await new Commands.QuerySingleCommit(_repo.FullPath, sha).GetResultAsync(); @@ -312,61 +412,6 @@ public async Task CherryPickAsync(Models.Commit commit) } } - public async Task RewordHeadAsync(Models.Commit head) - { - if (_repo.CanCreatePopup()) - { - var message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); - _repo.ShowPopup(new Reword(_repo, head, message)); - } - } - - public async Task SquashOrFixupHeadAsync(Models.Commit head, bool fixup) - { - if (head.Parents.Count == 1) - { - var parent = await new Commands.QuerySingleCommit(_repo.FullPath, head.Parents[0]).GetResultAsync(); - if (parent == null) - return; - - string message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.Parents[0]).GetResultAsync(); - if (!fixup) - { - var headMessage = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); - message = $"{message}\n\n{headMessage}"; - } - - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new SquashOrFixupHead(_repo, parent, message, fixup)); - } - } - - public async Task DropHeadAsync(Models.Commit head) - { - 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 (parent != null && _repo.CanCreatePopup()) - _repo.ShowPopup(new DropHead(_repo, head, parent)); - } - - public async Task InteractiveRebaseAsync(Models.Commit commit, Models.InteractiveRebaseAction act) - { - var prefill = new InteractiveRebasePrefill(commit.SHA, act); - var start = act switch - { - Models.InteractiveRebaseAction.Squash or Models.InteractiveRebaseAction.Fixup => $"{commit.SHA}~~", - _ => $"{commit.SHA}~", - }; - - 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)); - } - public async Task GetCommitFullMessageAsync(Models.Commit commit) { return await new Commands.QueryCommitFullMessage(_repo.FullPath, commit.SHA) @@ -382,7 +427,7 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) _repo.SearchCommitContext.Selected = null; head = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync(); if (head != null) - DetailContext = new RevisionCompare(_repo.FullPath, commit, head); + DetailContext = new RevisionCompare(_repo, commit, head); return null; } @@ -392,47 +437,108 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) public void CompareWithWorktree(Models.Commit commit) { - DetailContext = new RevisionCompare(_repo.FullPath, commit, null); + DetailContext = new RevisionCompare(_repo, commit, null); } - private void NavigateTo(Models.Commit commit) + private void PostCommitsChanged() { - AutoSelectedCommit = commit; + if (_selectedCommits.Count == 0) + return; - if (commit == null) + if (_commits.Count == 0 || _selectedCommits.Count > 20) { - DetailContext = null; + SelectedCommits = []; + return; } - else - { - NavigationId = _navigationId + 1; - if (_detailContext is CommitDetail detail) + var set = new HashSet(); + foreach (var c in _selectedCommits) + set.Add(c.SHA); + + var selected = new List(); + foreach (var c in _commits) + { + if (set.Contains(c.SHA)) { - detail.Commit = commit; + selected.Add(c); + set.Remove(c.SHA); + if (set.Count == 0) + break; } + } + + SelectedCommits = selected; + } + + private void PostSelectedCommitsChanged() + { + if (_ignoreSelectionChange) + return; + + if (_selectedCommits.Count == 0) + { + _repo.SearchCommitContext.Selected = null; + DetailContext = new Models.Null(); + } + else if (_selectedCommits.Count == 1) + { + var c = _selectedCommits[0]; + if (_repo.SearchCommitContext.Selected == null || !_repo.SearchCommitContext.Selected.SHA.Equals(c.SHA, StringComparison.Ordinal)) + _repo.SearchCommitContext.Selected = _repo.SearchCommitContext.Results?.Find(x => x.SHA.Equals(c.SHA, StringComparison.Ordinal)); + + if (_detailContext is CommitDetail detail) + detail.Commit = c; else - { - var commitDetail = new CommitDetail(_repo, _commitDetailSharedData); - commitDetail.Commit = commit; - DetailContext = commitDetail; - } + DetailContext = new CommitDetail(_repo, _commitDetailSharedData) { Commit = c }; } + else if (_selectedCommits.Count == 2) + { + _repo.SearchCommitContext.Selected = null; + + if (_detailContext is RevisionCompare compare) + compare.SetTargets(_selectedCommits[1], _selectedCommits[0]); + else + DetailContext = new RevisionCompare(_repo, _selectedCommits[1], _selectedCommits[0]); + } + else + { + _repo.SearchCommitContext.Selected = null; + DetailContext = new Models.Count(_selectedCommits.Count); + } + + if (_repo.UIStates.GraphHighlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + GenerateGraph(_commits); + } + + private void GenerateGraph(List commits, bool commitsChanged = false) + { + var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); + var highlighting = _repo.UIStates.GraphHighlighting; + var extraHeads = new HashSet(); + + if (highlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + { + foreach (var c in _selectedCommits) + extraHeads.Add(c.SHA); + } + + Graph = Models.CommitGraph.Generate(commits, commitsChanged, firstParentOnly, highlighting, extraHeads); } private Repository _repo = null; private CommitDetailSharedData _commitDetailSharedData = null; private bool _isLoading = true; - private List _commits = new List(); + private List _commits = []; private Models.CommitGraph _graph = null; - private Models.Commit _autoSelectedCommit = null; + private List _selectedCommits = []; private Models.Bisect _bisect = null; - private long _navigationId = 0; - private IDisposable _detailContext = 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); - private GridLength _bottomArea = new GridLength(1, GridUnitType.Star); + private object _detailContext = new Models.Null(); + private bool _ignoreSelectionChange = false; + + private GridLength _leftArea = new(1, GridUnitType.Star); + private GridLength _rightArea = new(1, GridUnitType.Star); + private GridLength _topArea = new(1, GridUnitType.Star); + private GridLength _bottomArea = new(1, GridUnitType.Star); + private bool _isCollapseDetails = false; } } diff --git a/src/ViewModels/ICommandPalette.cs b/src/ViewModels/ICommandPalette.cs index 232a8558d..e46829122 100644 --- a/src/ViewModels/ICommandPalette.cs +++ b/src/ViewModels/ICommandPalette.cs @@ -1,17 +1,21 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class ICommandPalette : ObservableObject, IDisposable + public class ICommandPalette : ObservableObject { - public void Dispose() + public void Open() { - Cleanup(); + var host = App.GetLauncher(); + if (host != null) + host.CommandPalette = this; } - public virtual void Cleanup() + public void Close() { + var host = App.GetLauncher(); + if (host != null) + host.CommandPalette = null; } } } diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index 5f0c88dd9..392615ab3 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -9,6 +9,7 @@ using Avalonia.Platform; using BitMiracle.LibTiff.Classic; using Pfim; +using StbImageSharp; namespace SourceGit.ViewModels { @@ -32,18 +33,30 @@ public static Models.ImageDecoder GetDecoder(string file) ".ico" or ".bmp" or ".gif" or ".jpg" or ".jpeg" or ".png" or ".webp" => Models.ImageDecoder.Builtin, ".tga" or ".dds" => Models.ImageDecoder.Pfim, ".tif" or ".tiff" => Models.ImageDecoder.Tiff, + ".psd" => Models.ImageDecoder.StbImage, _ => Models.ImageDecoder.None, }; } public static async Task FromFileAsync(string fullpath, Models.ImageDecoder decoder) { + if (!File.Exists(fullpath)) + return new ImageSource(null, 0); + await using var stream = File.OpenRead(fullpath); return await Task.Run(() => LoadFromStream(stream, decoder)).ConfigureAwait(false); } public static async Task FromRevisionAsync(string repo, string revision, string file, Models.ImageDecoder decoder) { + // If revision is empty, it means we are reading file in worktree. + if (string.IsNullOrEmpty(revision)) + return await FromFileAsync(Path.Combine(repo, file), decoder).ConfigureAwait(false); + + var emptyTreeHash = Models.EmptyTreeHash.Guess(revision); + if (emptyTreeHash.Equals(revision, StringComparison.Ordinal)) + return new ImageSource(null, 0); + await using var stream = await Commands.QueryFileContent.RunAsync(repo, revision, file).ConfigureAwait(false); return await Task.Run(() => LoadFromStream(stream, decoder)).ConfigureAwait(false); } @@ -77,6 +90,8 @@ private static ImageSource LoadFromStream(Stream stream, Models.ImageDecoder dec return DecodeWithPfim(stream, size); case Models.ImageDecoder.Tiff: return DecodeWithTiff(stream, size); + case Models.ImageDecoder.StbImage: + return DecodeWithStbImage(stream, size); } } catch (Exception e) @@ -108,6 +123,12 @@ 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: pixelFormat = PixelFormats.Bgr555; break; @@ -154,11 +175,23 @@ private static ImageSource DecodeWithPfim(Stream stream, long size) return new ImageSource(null, 0); } - var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0); - var pixelSize = new PixelSize(pfiImage.Width, pfiImage.Height); - var dpi = new Vector(96, 96); - var bitmap = new Bitmap(pixelFormat, alphaFormat, ptr, pixelSize, dpi, stride); - return new ImageSource(bitmap, size); + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0); + var pixelSize = new PixelSize(pfiImage.Width, pfiImage.Height); + var dpi = new Vector(96, 96); + var bitmap = new Bitmap(pixelFormat, alphaFormat, ptr, pixelSize, dpi, stride); + return new ImageSource(bitmap, size); + } + catch + { + return new ImageSource(null, 0); + } + finally + { + handle.Free(); + } } } @@ -184,5 +217,19 @@ private static ImageSource DecodeWithTiff(Stream stream, long size) return new ImageSource(bitmap, size); } } + + private static ImageSource DecodeWithStbImage(Stream stream, long size) + { + var image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + var data = image.Data; + var stride = image.Width * (int)image.Comp; + var pixelSize = new PixelSize(image.Width, image.Height); + var dpi = new Vector(96, 96); + var bitmap = new WriteableBitmap(pixelSize, dpi, PixelFormats.Rgba8888, AlphaFormat.Unpremul); + + using var frameBuffer = bitmap.Lock(); + Marshal.Copy(data, 0, frameBuffer.Address, data.Length); + return new ImageSource(bitmap, size); + } } } diff --git a/src/ViewModels/InProgressContexts.cs b/src/ViewModels/InProgressContexts.cs index 446cbe989..97699eeab 100644 --- a/src/ViewModels/InProgressContexts.cs +++ b/src/ViewModels/InProgressContexts.cs @@ -5,22 +5,34 @@ namespace SourceGit.ViewModels { public abstract class InProgressContext { - public async Task ContinueAsync() + public string Name + { + get; + protected set; + } + + public async Task ContinueAsync(CommandLog log) { if (_continueCmd != null) - await _continueCmd.ExecAsync(); + await _continueCmd.Use(log).ExecAsync(); } - public async Task SkipAsync() + public async Task SkipAsync(CommandLog log) { if (_skipCmd != null) - await _skipCmd.ExecAsync(); + await _skipCmd.Use(log).ExecAsync(); } - public async Task AbortAsync() + public async Task AbortAsync(CommandLog log) { if (_abortCmd != null) - await _abortCmd.ExecAsync(); + await _abortCmd.Use(log).ExecAsync(); + + OnAborted(); + } + + protected virtual void OnAborted() + { } protected Commands.Command _continueCmd = null; @@ -42,11 +54,14 @@ public string HeadName public CherryPickInProgress(Repository repo) { + Name = "Cherry-Pick"; + _continueCmd = new Commands.Command { WorkingDirectory = repo.FullPath, Context = repo.FullPath, - Args = "cherry-pick --continue", + Editor = Commands.Command.EditorType.None, + Args = "-c core.commentChar=\"^\" -c core.commentString=\"±\" cherry-pick --continue", }; _skipCmd = new Commands.Command @@ -93,12 +108,15 @@ public Models.Commit Onto public RebaseInProgress(Repository repo) { + _gitDir = repo.GitDir; + Name = "Rebase"; + _continueCmd = new Commands.Command { WorkingDirectory = repo.FullPath, Context = repo.FullPath, Editor = Commands.Command.EditorType.RebaseEditor, - Args = "rebase --continue", + Args = "-c core.commentChar=\"^\" -c core.commentString=\"±\" rebase --continue", }; _skipCmd = new Commands.Command @@ -113,6 +131,7 @@ public RebaseInProgress(Repository repo) WorkingDirectory = repo.FullPath, Context = repo.FullPath, Args = "rebase --abort", + RaiseError = false, }; HeadName = File.ReadAllText(Path.Combine(repo.GitDir, "rebase-merge", "head-name")).Trim(); @@ -133,6 +152,23 @@ public RebaseInProgress(Repository repo) Onto = new Commands.QuerySingleCommit(repo.FullPath, ontoSHA).GetResult() ?? new Models.Commit() { SHA = ontoSHA }; BaseName = Onto.GetFriendlyName(); } + + protected override void OnAborted() + { + var rebaseMergeDir = Path.Combine(_gitDir, "rebase-merge"); + if (Directory.Exists(rebaseMergeDir)) + Directory.Delete(rebaseMergeDir, true); + + var rebaseApplyDir = Path.Combine(_gitDir, "rebase-apply"); + if (Directory.Exists(rebaseApplyDir)) + Directory.Delete(rebaseApplyDir, true); + + var jobFile = Path.Combine(_gitDir, "sourcegit.interactive_rebase"); + if (File.Exists(jobFile)) + File.Delete(jobFile); + } + + private readonly string _gitDir; } public class RevertInProgress : InProgressContext @@ -144,11 +180,14 @@ public Models.Commit Head public RevertInProgress(Repository repo) { + Name = "Revert"; + _continueCmd = new Commands.Command { WorkingDirectory = repo.FullPath, Context = repo.FullPath, - Args = "revert --continue", + Editor = Commands.Command.EditorType.None, + Args = "-c core.commentChar=\"^\" -c core.commentString=\"±\" revert --continue", }; _skipCmd = new Commands.Command @@ -189,11 +228,14 @@ public string SourceName public MergeInProgress(Repository repo) { + Name = "Merge"; + _continueCmd = new Commands.Command { WorkingDirectory = repo.FullPath, Context = repo.FullPath, - Args = "merge --continue", + Editor = Commands.Command.EditorType.None, + Args = "-c core.commentChar=\"^\" -c core.commentString=\"±\" merge --continue", }; _abortCmd = new Commands.Command diff --git a/src/ViewModels/Init.cs b/src/ViewModels/Init.cs index f6740d0f6..882d3d647 100644 --- a/src/ViewModels/Init.cs +++ b/src/ViewModels/Init.cs @@ -16,12 +16,15 @@ public string Reason private set; } - public Init(string pageId, string path, RepositoryNode parent, string reason) + public Init(string pageId, string path, RepositoryNode parent, int bookmark, string reason) { _pageId = pageId; _targetPath = path; _parentNode = parent; - Reason = string.IsNullOrEmpty(reason) ? "Invalid repository detected!" : reason; + _bookmark = bookmark; + + Reason = string.IsNullOrEmpty(reason) ? "unknown error" : reason; + Reason = Reason.Trim(); } public override async Task Sure() @@ -40,6 +43,7 @@ public override async Task Sure() if (succ) { var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(_targetPath, _parentNode, true); + node.Bookmark = _bookmark; await node.UpdateStatusAsync(false, null); Welcome.Instance.Refresh(); @@ -50,5 +54,6 @@ public override async Task Sure() private readonly string _pageId = null; private string _targetPath = null; private readonly RepositoryNode _parentNode = null; + private readonly int _bookmark = 0; } } diff --git a/src/ViewModels/InitGitFlow.cs b/src/ViewModels/InitGitFlow.cs index 6e425879a..f00ad01dd 100644 --- a/src/ViewModels/InitGitFlow.cs +++ b/src/ViewModels/InitGitFlow.cs @@ -11,16 +11,16 @@ public partial class InitGitFlow : Popup [GeneratedRegex(@"^[\w\-/\.]+$")] private static partial Regex REG_TAG_PREFIX(); - [Required(ErrorMessage = "Master branch name is required!!!")] + [Required(ErrorMessage = "Production branch name is required!!!")] [RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))] - public string Master + public string Production { - get => _master; - set => SetProperty(ref _master, value, true); + get => _production; + set => SetProperty(ref _production, value, true); } - [Required(ErrorMessage = "Develop branch name is required!!!")] + [Required(ErrorMessage = "Development branch name is required!!!")] [RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))] public string Develop @@ -72,21 +72,21 @@ public InitGitFlow(Repository repo) } if (localBranches.Contains("master")) - _master = "master"; + _production = "master"; else if (localBranches.Contains("main")) - _master = "main"; + _production = "main"; else if (localBranches.Count > 0) - _master = localBranches[0]; + _production = localBranches[0]; else - _master = "master"; + _production = "main"; } public static ValidationResult ValidateBaseBranch(string _, ValidationContext ctx) { if (ctx.ObjectInstance is InitGitFlow initializer) { - if (initializer._master == initializer._develop) - return new ValidationResult("Develop branch has the same name with master branch!"); + if (initializer._production == initializer._develop) + return new ValidationResult("Develop branch has the same name with production branch!"); } return ValidationResult.Success; @@ -111,10 +111,10 @@ public override async Task Sure() bool succ; var current = _repo.CurrentBranch; - var masterBranch = _repo.Branches.Find(x => x.IsLocal && x.Name.Equals(_master, StringComparison.Ordinal)); - if (masterBranch == null) + var productionBranch = _repo.Branches.Find(x => x.IsLocal && x.Name.Equals(_production, StringComparison.Ordinal)); + if (productionBranch == null) { - succ = await new Commands.Branch(_repo.FullPath, _master) + succ = await new Commands.Branch(_repo.FullPath, _production) .Use(log) .CreateAsync(current.Head, true); if (!succ) @@ -137,23 +137,17 @@ public override async Task Sure() } } - succ = await Commands.GitFlow.InitAsync( - _repo.FullPath, - _master, - _develop, - _featurePrefix, - _releasePrefix, - _hotfixPrefix, - _tagPrefix, - log); + succ = await new Commands.GitFlow(_repo.FullPath) + .Use(log) + .InitAsync(_production, _develop, _featurePrefix, _releasePrefix, _hotfixPrefix, _tagPrefix); log.Complete(); if (succ) { var gitflow = new Models.GitFlow(); - gitflow.Master = _master; - gitflow.Develop = _develop; + gitflow.ProductionBranch = _production; + gitflow.DevelopmentBranch = _develop; gitflow.FeaturePrefix = _featurePrefix; gitflow.ReleasePrefix = _releasePrefix; gitflow.HotfixPrefix = _hotfixPrefix; @@ -164,7 +158,7 @@ public override async Task Sure() } private readonly Repository _repo; - private string _master; + private string _production; private string _develop = "develop"; private string _featurePrefix = "feature/"; private string _releasePrefix = "release/"; diff --git a/src/ViewModels/InteractiveRebase.cs b/src/ViewModels/InteractiveRebase.cs index 1c85e006b..a8f013fe4 100644 --- a/src/ViewModels/InteractiveRebase.cs +++ b/src/ViewModels/InteractiveRebase.cs @@ -13,6 +13,7 @@ namespace SourceGit.ViewModels { public record InteractiveRebasePrefill(string SHA, Models.InteractiveRebaseAction Action); + public record InteractiveRebaseReorderItem(string Key, InteractiveRebaseItem Item); public class InteractiveRebaseItem : ObservableObject { @@ -76,12 +77,6 @@ public bool ShowEditMessageButton set => SetProperty(ref _showEditMessageButton, value); } - public bool IsFullMessageUsed - { - get => _isFullMessageUsed; - set => SetProperty(ref _isFullMessageUsed, value); - } - public Thickness DropDirectionIndicator { get => _dropDirectionIndicator; @@ -108,7 +103,6 @@ public InteractiveRebaseItem(int order, Models.Commit c, string message) private string _fullMessage; private bool _canSquashOrFixup = true; private bool _showEditMessageButton = false; - private bool _isFullMessageUsed = true; private Thickness _dropDirectionIndicator = new Thickness(0); } @@ -131,6 +125,12 @@ public bool AutoStash set; } = true; + public bool NoVerify + { + get; + set; + } + public AvaloniaList IssueTrackers { get => _repo.IssueTrackers; @@ -179,10 +179,85 @@ public InteractiveRebase(Repository repo, Models.Commit on, InteractiveRebasePre .ConfigureAwait(false); var list = new List(); + var needReorder = new List(); + var needReorderKeySet = new HashSet(); + + // First-pass to find out the commits that need to be reordered and leave the reset in original order. for (var i = 0; i < commits.Count; i++) { var c = commits[i]; - list.Add(new InteractiveRebaseItem(commits.Count - i, c.Commit, c.Message)); + var item = new InteractiveRebaseItem(commits.Count - i, c.Commit, c.Message); + var subject = c.Commit.Subject; + + if (item.OriginalOrder > 1) + { + if (subject.StartsWith("fixup! ", StringComparison.Ordinal)) + { + item.Action = Models.InteractiveRebaseAction.Fixup; + + var targetSubject = subject.Substring(7).Trim(); + needReorder.Add(new(targetSubject, item)); + needReorderKeySet.Add(targetSubject); + continue; + } + + if (subject.StartsWith("squash! ", StringComparison.Ordinal)) + { + item.Action = Models.InteractiveRebaseAction.Squash; + + var targetSubject = subject.Substring(8).Trim(); + needReorder.Add(new(targetSubject, item)); + needReorderKeySet.Add(targetSubject); + continue; + } + } + + list.Add(item); + } + + // Second-pass to find the place for reordered commits and insert them. + for (var i = list.Count - 1; i >= 0; i--) + { + var subject = list[i].Commit.Subject.Trim(); + if (!needReorderKeySet.Contains(subject)) + continue; + + for (var j = needReorder.Count - 1; j >= 0; j--) + { + var test = needReorder[j]; + if (subject.Equals(test.Key, StringComparison.Ordinal)) + { + list.Insert(i, test.Item); + needReorder.RemoveAt(j); + } + } + + needReorderKeySet.Remove(subject); + if (needReorderKeySet.Count == 0) + break; + } + + // Last-pass to insert the remaining commits that are marked as fixup!/squash! but their target commit is not found. + foreach (var v in needReorder) + { + // For safety, reset to pick if the target commit is not found + v.Item.Action = Models.InteractiveRebaseAction.Pick; + + // Find out the first picked (exclude reordered) commit that should be picked after this commit + var insertPos = 0; + for (var i = 0; i < list.Count; i++) + { + var item = list[i]; + if (item.Action == Models.InteractiveRebaseAction.Pick) + { + if (v.Item.OriginalOrder < item.OriginalOrder) + insertPos = i + 1; + else + break; + } + } + + list.Insert(insertPos, v.Item); } var selected = list.Count > 0 ? list[0] : null; @@ -315,7 +390,7 @@ public async Task Start() } var log = _repo.CreateLog("Interactive Rebase"); - var succ = await new Commands.InteractiveRebase(_repo.FullPath, On.SHA, AutoStash) + var succ = await new Commands.InteractiveRebase(_repo.FullPath, On.SHA, AutoStash, NoVerify) .Use(log) .ExecAsync(); @@ -354,7 +429,6 @@ private void UpdateItems() 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; @@ -365,14 +439,24 @@ private void UpdateItems() 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); + { + if (item.OriginalFullMessage.StartsWith("squash! ", StringComparison.Ordinal)) + { + var firstLineEnd = item.OriginalFullMessage.IndexOf('\n'); + if (firstLineEnd > 0) + pendingMessages.Add(item.OriginalFullMessage.Substring(firstLineEnd + 1)); + } + else + { + pendingMessages.Add(item.OriginalFullMessage); + } + } hasPending = true; continue; @@ -382,7 +466,6 @@ private void UpdateItems() item.Action == Models.InteractiveRebaseAction.Edit) { var oldPendingType = item.PendingType; - item.IsFullMessageUsed = true; item.ShowEditMessageButton = true; item.PendingType = hasPending ? Models.InteractiveRebasePendingType.Target : Models.InteractiveRebasePendingType.None; @@ -412,20 +495,28 @@ private void UpdateItems() 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(); + if (pendingMessages.Count > 0) + { + 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(); + } + else + { + item.PendingType = Models.InteractiveRebasePendingType.Target; + item.ShowEditMessageButton = false; + item.FullMessage = item.OriginalFullMessage; + } hasPending = false; pendingMessages.Clear(); diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 4d3a59111..7b17290a7 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -35,7 +35,7 @@ public LauncherPage ActivePage set { if (SetProperty(ref _activePage, value)) - PostActivePageChanged(value?.Data as Repository); + PostActivePageChanged(); } } @@ -45,54 +45,74 @@ public ICommandPalette CommandPalette set => SetProperty(ref _commandPalette, value); } + public Models.Version NewVersion + { + get => _newVersion; + set => SetProperty(ref _newVersion, value); + } + public Launcher(string startupRepo) { + Models.Notification.Raised += DispatchNotification; _ignoreIndexChange = true; + ActiveWorkspace = Preferences.Instance.GetActiveWorkspace(); Pages = new AvaloniaList(); AddNewTab(); - var pref = Preferences.Instance; - ActiveWorkspace = pref.GetActiveWorkspace(); - var repos = ActiveWorkspace.Repositories.ToArray(); foreach (var repo in repos) + OpenRepositoryInTab(repo, null); + + _ignoreIndexChange = false; + + if (!TryOpenRepositoryFromPath(startupRepo)) { - var node = pref.FindNode(repo) ?? - new RepositoryNode - { - Id = repo, - Name = Path.GetFileName(repo), - Bookmark = 0, - IsRepository = true, - }; - - OpenRepositoryInTab(node, null); + var activeIdx = ActiveWorkspace.ActiveIdx; + if (activeIdx > 0 && activeIdx < Pages.Count) + ActivePage = Pages[activeIdx]; + else + ActivePage = Pages[0]; } - _ignoreIndexChange = false; + PostActivePageChanged(); + } - if (!string.IsNullOrEmpty(startupRepo)) + public bool TryOpenRepositoryFromPath(string repo) + { + if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) { - var test = new Commands.QueryRepositoryRootPath(startupRepo).GetResult(); - if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) + var isBare = new Commands.IsBareRepository(repo).GetResult(); + if (isBare) { - var node = pref.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(repo, null, false); Welcome.Instance.Refresh(); + OpenRepositoryInTab(node, null); + return true; + } + var test = new Commands.QueryRepositoryRootPath(repo).GetResult(); + if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) + { + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); + Welcome.Instance.Refresh(); OpenRepositoryInTab(node, null); - return; + return true; + } + else + { + if (ActivePage is not { Data: Welcome { }, Popup: null }) + AddNewTab(); + + ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository."); + return true; } } - var activeIdx = ActiveWorkspace.ActiveIdx; - if (activeIdx >= 0 && activeIdx < Pages.Count) - ActivePage = Pages[activeIdx]; - else - ActivePage = Pages[0]; + return false; } - public void Quit() + public void CloseAll() { _ignoreIndexChange = true; @@ -107,15 +127,6 @@ public void SwitchWorkspace(Workspace to) if (to == null || to.IsActive) return; - foreach (var one in Pages) - { - if (!one.CanCreatePopup() || one.Data is Repository { IsAutoFetching: true }) - { - App.RaiseException(null, "You have unfinished task(s) in opened pages. Please wait!!!"); - return; - } - } - _ignoreIndexChange = true; var pref = Preferences.Instance; @@ -133,20 +144,7 @@ public void SwitchWorkspace(Workspace to) var repos = to.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); - } - - _ignoreIndexChange = false; + OpenRepositoryInTab(repo, null); var activeIdx = to.ActiveIdx; if (activeIdx >= 0 && activeIdx < Pages.Count) @@ -154,6 +152,8 @@ public void SwitchWorkspace(Workspace to) else ActivePage = Pages[0]; + _ignoreIndexChange = false; + PostActivePageChanged(); Preferences.Instance.Save(); GC.Collect(); } @@ -214,6 +214,8 @@ public void CloseTab(LauncherPage page) _activeWorkspace.Repositories.Clear(); _activeWorkspace.ActiveIdx = 0; + if (last.Node.IsUnmanaged) + last.Node.SaveMinimalInfo(repo.GitDir); repo.Close(); Welcome.Instance.ClearSearchFilter(); @@ -222,7 +224,7 @@ public void CloseTab(LauncherPage page) last.Popup?.Cleanup(); last.Popup = null; - PostActivePageChanged(null); + PostActivePageChanged(); GC.Collect(); } else @@ -282,6 +284,21 @@ public void CloseRightTabs() GC.Collect(); } + public void OpenRepositoryInTab(string repo, LauncherPage page) + { + var normalizedPath = repo.Replace('\\', '/').TrimEnd('/'); + var node = Preferences.Instance.FindNode(normalizedPath) ?? new RepositoryNode + { + Id = normalizedPath, + Name = Path.GetFileName(normalizedPath), + Bookmark = 0, + IsRepository = true, + IsUnmanaged = true + }; + + OpenRepositoryInTab(node, null); + } + public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) { foreach (var one in Pages) @@ -293,9 +310,14 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) } } - if (!Path.Exists(node.Id)) + if (!Directory.Exists(node.Id)) { - App.RaiseException(node.Id, "Repository does NOT exist any more. Please remove it."); + ActivePage.Notifications.Add(new Models.Notification + { + Group = node.Id, + Message = "Repository does NOT exist any more. Please remove it.", + IsError = true, + }); return; } @@ -303,10 +325,18 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) var gitDir = isBare ? node.Id : GetRepositoryGitDir(node.Id); if (string.IsNullOrEmpty(gitDir)) { - App.RaiseException(node.Id, "Given path is not a valid git repository!"); + ActivePage.Notifications.Add(new Models.Notification + { + Group = node.Id, + Message = "Given path is not a valid git repository!", + IsError = true, + }); return; } + if (node.IsUnmanaged) + node.LoadMinimalInfo(gitDir); + var repo = new Repository(isBare, node.Id, gitDir); repo.Open(); @@ -338,46 +368,29 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) } if (_activePage == page) - PostActivePageChanged(repo); + 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) + private void DispatchNotification(Models.Notification notification) { if (!Dispatcher.UIThread.CheckAccess()) { - Dispatcher.UIThread.Invoke(() => DispatchNotification(pageId, message, isError)); + Dispatcher.UIThread.Invoke(() => DispatchNotification(notification)); return; } - var notification = new Models.Notification() + if (string.IsNullOrEmpty(notification.Group)) { - IsError = isError, - Message = message, - }; + _activePage?.Notifications.Add(notification); + return; + } foreach (var page in Pages) { var id = page.Node.Id.Replace('\\', '/').TrimEnd('/'); - if (id == pageId) + if (id.Equals(notification.Group, StringComparison.OrdinalIgnoreCase)) { page.Notifications.Add(notification); return; @@ -425,6 +438,9 @@ private void CloseRepositoryInTab(LauncherPage page, bool removeFromWorkspace = if (removeFromWorkspace) _activeWorkspace.Repositories.Remove(repo.FullPath); + if (page.Node.IsUnmanaged) + page.Node.SaveMinimalInfo(repo.GitDir); + repo.Close(); } @@ -433,51 +449,30 @@ private void CloseRepositoryInTab(LauncherPage page, bool removeFromWorkspace = page.Data = null; } - private void PostActivePageChanged(Repository repo) + private void PostActivePageChanged() { if (_ignoreIndexChange) return; - var builder = new StringBuilder(512); - if (_activeWorkspace != null) - { - var workspaces = Preferences.Instance.Workspaces; - if (workspaces.Count == 0 || workspaces.Count > 1 || workspaces[0] != _activeWorkspace) - builder.Append('[').Append(_activeWorkspace.Name).Append("] "); - } - - if (_activePage is { Data: Repository }) - { - var node = _activePage.Node; - var name = node.Name; - var path = node.Id; + if (_activePage is { Data: Repository repo }) + _activeWorkspace.ActiveIdx = _activeWorkspace.Repositories.IndexOf(repo.FullPath); - 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)}"; - } + var builder = new StringBuilder(512); + builder.Append(string.IsNullOrEmpty(_activePage.Node.Name) ? "Repositories" : _activePage.Node.Name); - builder.Append(name).Append(" (").Append(path).Append(')'); - } - else - { - builder.Append("Repositories"); - } + var workspaces = Preferences.Instance.Workspaces; + if (workspaces.Count == 0 || workspaces.Count > 1 || workspaces[0] != _activeWorkspace) + builder.Append(" - ").Append(_activeWorkspace.Name); - CancelCommandPalette(); Title = builder.ToString(); - - if (repo != null) - _activeWorkspace.ActiveIdx = _activeWorkspace.Repositories.IndexOf(repo.FullPath); + CommandPalette = null; } - 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 ICommandPalette _commandPalette = null; + private ICommandPalette _commandPalette; + private Models.Version _newVersion = null; } } diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 11ba601ce..db5e0241b 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -1,8 +1,6 @@ using System; using System.Threading.Tasks; - using Avalonia.Collections; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -59,12 +57,6 @@ public void ClearNotifications() Notifications.Clear(); } - public async Task CopyPathAsync() - { - if (_node.IsRepository) - await App.CopyTextAsync(_node.Id); - } - public void ChangeDirtyState(Models.DirtyState flag, bool remove) { var state = _dirtyState; @@ -106,7 +98,7 @@ public async Task ProcessPopupAsync() } catch (Exception e) { - App.LogException(e); + Native.OS.LogException(e); } dump.InProgress = false; diff --git a/src/ViewModels/LauncherPagesCommandPalette.cs b/src/ViewModels/LauncherPagesCommandPalette.cs index 45a065da3..265aa0704 100644 --- a/src/ViewModels/LauncherPagesCommandPalette.cs +++ b/src/ViewModels/LauncherPagesCommandPalette.cs @@ -60,17 +60,6 @@ public LauncherPagesCommandPalette(Launcher launcher) 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; @@ -78,12 +67,15 @@ public void ClearFilter() public void OpenOrSwitchTo() { + _opened.Clear(); + _visiblePages.Clear(); + _visibleRepos.Clear(); + Close(); + if (_selectedPage != null) _launcher.ActivePage = _selectedPage; else if (_selectedRepo != null) _launcher.OpenRepositoryInTab(_selectedRepo, null); - - _launcher?.CancelCommandPalette(); } private void UpdateVisible() diff --git a/src/ViewModels/LayoutInfo.cs b/src/ViewModels/LayoutInfo.cs index 26ae128db..f993e242e 100644 --- a/src/ViewModels/LayoutInfo.cs +++ b/src/ViewModels/LayoutInfo.cs @@ -65,17 +65,10 @@ public GridLength CommitDetailFilesLeftWidth set => SetProperty(ref _commitDetailFilesLeftWidth, value); } - public DataGridLength AuthorColumnWidth - { - get => _authorColumnWidth; - set => SetProperty(ref _authorColumnWidth, value); - } - private GridLength _repositorySidebarWidth = new GridLength(250, GridUnitType.Pixel); private GridLength _workingCopyLeftWidth = new GridLength(300, GridUnitType.Pixel); private GridLength _stashesLeftWidth = new GridLength(300, GridUnitType.Pixel); private GridLength _commitDetailChangesLeftWidth = new GridLength(256, GridUnitType.Pixel); private GridLength _commitDetailFilesLeftWidth = new GridLength(256, GridUnitType.Pixel); - private DataGridLength _authorColumnWidth = new DataGridLength(120, DataGridLengthUnitType.Pixel, 120, 120); } } diff --git a/src/ViewModels/Merge.cs b/src/ViewModels/Merge.cs index 612eb6311..35f9567f6 100644 --- a/src/ViewModels/Merge.cs +++ b/src/ViewModels/Merge.cs @@ -1,7 +1,18 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Threading; namespace SourceGit.ViewModels { + public enum MergeTestingState + { + Disabled = 0, + Testing, + WillCauseConflicts, + UnknownError, + NoConflicts, + } + public class Merge : Popup { public object Source @@ -16,8 +27,18 @@ 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.NoFastForward; + } + } + + public bool CanEditMessage + { + get => _canEditMessage; + set => SetProperty(ref _canEditMessage, value); } public bool Edit @@ -26,6 +47,12 @@ public bool Edit set; } = false; + public MergeTestingState TestingState + { + get => _testingState; + private set => SetProperty(ref _testingState, value); + } + public Merge(Repository repo, Models.Branch source, string into, bool forceFastForward) { _repo = repo; @@ -34,6 +61,9 @@ public Merge(Repository repo, Models.Branch source, string into, bool forceFastF Source = source; Into = into; Mode = forceFastForward ? Models.MergeMode.FastForward : AutoSelectMergeMode(); + + if (!forceFastForward) + Test(); } public Merge(Repository repo, Models.Commit source, string into) @@ -44,6 +74,8 @@ public Merge(Repository repo, Models.Commit source, string into) Source = source; Into = into; Mode = AutoSelectMergeMode(); + + Test(); } public Merge(Repository repo, Models.Tag source, string into) @@ -54,6 +86,8 @@ public Merge(Repository repo, Models.Tag source, string into) Source = source; Into = into; Mode = AutoSelectMergeMode(); + + Test(); } public override async Task Sure() @@ -65,17 +99,20 @@ public override async Task Sure() var log = _repo.CreateLog($"Merging '{_sourceName}' into '{Into}'"); Use(log); - var succ = 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 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 squashMsgFile = Path.Combine(_repo.GitDir, "SQUASH_MSG"); + if (Mode == Models.MergeMode.Squash && File.Exists(squashMsgFile)) + { + var msg = await File.ReadAllTextAsync(squashMsgFile); + _repo.SetCommitMessage(msg); + } + + await _repo.AutoUpdateSubmodulesAsync(log); } log.Complete(); @@ -111,7 +148,32 @@ private Models.MergeMode AutoSelectMergeMode() return Models.MergeMode.Supported[preferredMergeModeIdx]; } + private void Test() + { + if (Native.OS.GitVersion < Models.GitVersions.TESTING_MERGE) + return; + + TestingState = MergeTestingState.Testing; + + Task.Run(async () => + { + var exitCode = await new Commands.MergeTree(_repo.FullPath, _sourceName, Into) + .GetExitCodeAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => TestingState = exitCode switch + { + 0 => MergeTestingState.NoConflicts, + 1 => MergeTestingState.WillCauseConflicts, + _ => MergeTestingState.UnknownError, + }); + }); + } + private readonly Repository _repo = null; private readonly string _sourceName; + private Models.MergeMode _mode = Models.MergeMode.Default; + private bool _canEditMessage = true; + private MergeTestingState _testingState = MergeTestingState.Disabled; } } diff --git a/src/ViewModels/MergeCommandPalette.cs b/src/ViewModels/MergeCommandPalette.cs index ecab68cb0..745bcbc07 100644 --- a/src/ViewModels/MergeCommandPalette.cs +++ b/src/ViewModels/MergeCommandPalette.cs @@ -27,22 +27,12 @@ public Models.Branch SelectedBranch set => SetProperty(ref _selectedBranch, value); } - public MergeCommandPalette(Launcher launcher, Repository repo) + public MergeCommandPalette(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; @@ -50,10 +40,11 @@ public void ClearFilter() public void Launch() { + _branches.Clear(); + Close(); + if (_repo.CanCreatePopup() && _selectedBranch != null) _repo.ShowPopup(new Merge(_repo, _selectedBranch, _repo.CurrentBranch.Name, false)); - - _launcher?.CancelCommandPalette(); } private void UpdateBranches() @@ -75,7 +66,7 @@ private void UpdateBranches() branches.Sort((l, r) => { if (l.IsLocal == r.IsLocal) - return l.Name.CompareTo(r.Name); + return Models.NumericSort.Compare(l.Name, r.Name); return l.IsLocal ? -1 : 1; }); @@ -90,7 +81,6 @@ private void UpdateBranches() SelectedBranch = autoSelected; } - private Launcher _launcher = null; private Repository _repo = null; private List _branches = new List(); private string _filter = string.Empty; diff --git a/src/ViewModels/MergeConflictEditor.cs b/src/ViewModels/MergeConflictEditor.cs new file mode 100644 index 000000000..fc9d0f632 --- /dev/null +++ b/src/ViewModels/MergeConflictEditor.cs @@ -0,0 +1,484 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +using Avalonia; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class MergeConflictEditor : ObservableObject + { + public string FilePath + { + get => _filePath; + } + + public object Mine + { + get; + } + + public object Theirs + { + get; + } + + public string Error + { + get => _error; + private set => SetProperty(ref _error, value); + } + + public List OursLines + { + get => _oursLines; + private set => SetProperty(ref _oursLines, value); + } + + public List TheirsLines + { + get => _theirsLines; + private set => SetProperty(ref _theirsLines, value); + } + + public List ResultLines + { + get => _resultLines; + private set => SetProperty(ref _resultLines, value); + } + + public int MaxLineNumber + { + get => _maxLineNumber; + private set => SetProperty(ref _maxLineNumber, value); + } + + public int UnsolvedCount + { + get => _unsolvedCount; + private set => SetProperty(ref _unsolvedCount, value); + } + + public Vector ScrollOffset + { + get => _scrollOffset; + set => SetProperty(ref _scrollOffset, value); + } + + public Models.ConflictSelectedChunk SelectedChunk + { + get => _selectedChunk; + set => SetProperty(ref _selectedChunk, value); + } + + public IReadOnlyList ConflictRegions + { + get => _conflictRegions; + } + + public MergeConflictEditor(Repository repo, Models.Commit head, string filePath) + { + _repo = repo; + _filePath = filePath; + + (Mine, Theirs) = repo.InProgressContext switch + { + CherryPickInProgress cherryPick => (head, cherryPick.Head), + RebaseInProgress rebase => (rebase.Onto, rebase.StoppedAt), + RevertInProgress revert => (head, revert.Head), + MergeInProgress merge => (head, merge.Source), + _ => (head, (object)"Stash or Patch"), + }; + + var workingCopyPath = Path.Combine(_repo.FullPath, _filePath); + var workingCopyContent = string.Empty; + if (File.Exists(workingCopyPath)) + workingCopyContent = File.ReadAllText(workingCopyPath); + + if (workingCopyContent.IndexOf('\0', StringComparison.Ordinal) >= 0) + { + _error = "Binary file is not supported."; + return; + } + + ParseOriginalContent(workingCopyContent); + RefreshDisplayData(); + } + + public Models.ConflictLineState GetLineState(int line) + { + if (line >= 0 && line < _lineStates.Count) + return _lineStates[line]; + return Models.ConflictLineState.Normal; + } + + public void Resolve(object param) + { + if (_selectedChunk == null) + return; + + var region = _conflictRegions[_selectedChunk.ConflictIndex]; + if (param is not Models.ConflictResolution resolution) + return; + + // Try to resolve a resolved region. + if (resolution != Models.ConflictResolution.None && region.IsResolved) + return; + + // Try to undo an unresolved region. + if (resolution == Models.ConflictResolution.None && !region.IsResolved) + return; + + region.IsResolved = resolution != Models.ConflictResolution.None; + region.ResolutionType = resolution; + RefreshDisplayData(); + } + + public async Task SaveAndStageAsync() + { + if (_conflictRegions.Count == 0) + return true; + + if (_unsolvedCount > 0) + { + Error = "Cannot save: there are still unresolved conflicts."; + return false; + } + + var lines = _originalContent.Split('\n', StringSplitOptions.None); + var builder = new StringBuilder(); + var lastLineIdx = 0; + + foreach (var r in _conflictRegions) + { + for (var i = lastLineIdx; i < r.StartLineInOriginal; i++) + builder.Append(lines[i]).Append('\n'); + + if (r.ResolutionType == Models.ConflictResolution.UseOurs) + { + foreach (var l in r.OursContent) + builder.Append(l).Append('\n'); + } + else if (r.ResolutionType == Models.ConflictResolution.UseTheirs) + { + foreach (var l in r.TheirsContent) + builder.Append(l).Append('\n'); + } + else if (r.ResolutionType == Models.ConflictResolution.UseBothMineFirst) + { + foreach (var l in r.OursContent) + builder.Append(l).Append('\n'); + + foreach (var l in r.TheirsContent) + builder.Append(l).Append('\n'); + } + else if (r.ResolutionType == Models.ConflictResolution.UseBothTheirsFirst) + { + foreach (var l in r.TheirsContent) + builder.Append(l).Append('\n'); + + foreach (var l in r.OursContent) + builder.Append(l).Append('\n'); + } + + lastLineIdx = r.EndLineInOriginal + 1; + } + + for (var j = lastLineIdx; j < lines.Length; j++) + builder.Append(lines[j]).Append('\n'); + + if (builder.Length > 1) + builder.Length--; // Remove extra '\n' + + try + { + // Write merged content to file + var fullPath = Path.Combine(_repo.FullPath, _filePath); + await File.WriteAllTextAsync(fullPath, builder.ToString()); + + // Stage the file + var pathSpecFile = Path.GetTempFileName(); + await File.WriteAllTextAsync(pathSpecFile, _filePath); + await new Commands.Add(_repo.FullPath, pathSpecFile).ExecAsync(); + File.Delete(pathSpecFile); + + _repo.MarkWorkingCopyDirtyManually(); + return true; + } + catch (Exception ex) + { + Error = $"Failed to save and stage: {ex.Message}"; + return false; + } + } + + public void ClearErrorMessage() + { + Error = string.Empty; + } + + private void ParseOriginalContent(string content) + { + _originalContent = content; + _conflictRegions.Clear(); + + if (string.IsNullOrEmpty(content)) + return; + + var lines = content.Split('\n', StringSplitOptions.None); + var oursLines = new List(); + var theirsLines = new List(); + int oursLineNumber = 1; + int theirsLineNumber = 1; + int i = 0; + + while (i < lines.Length) + { + var line = lines[i]; + + if (line.StartsWith("<<<<<<<", StringComparison.Ordinal)) + { + var region = new Models.ConflictRegion + { + StartLineInOriginal = i, + StartMarker = line, + }; + + oursLines.Add(new()); + theirsLines.Add(new()); + i++; + + // Collect ours content + while (i < lines.Length && + !lines[i].StartsWith("|||||||", StringComparison.Ordinal) && + !lines[i].StartsWith("=======", StringComparison.Ordinal)) + { + line = lines[i]; + region.OursContent.Add(line); + oursLines.Add(new(Models.ConflictLineType.Ours, line, oursLineNumber++)); + theirsLines.Add(new()); + i++; + } + + // Skip diff3 base section if present + if (i < lines.Length && lines[i].StartsWith("|||||||", StringComparison.Ordinal)) + { + i++; + while (i < lines.Length && !lines[i].StartsWith("=======", StringComparison.Ordinal)) + i++; + } + + // Capture separator marker + if (i < lines.Length && lines[i].StartsWith("=======", StringComparison.Ordinal)) + { + oursLines.Add(new()); + theirsLines.Add(new()); + region.SeparatorMarker = lines[i]; + i++; + } + + // Collect theirs content + while (i < lines.Length && !lines[i].StartsWith(">>>>>>>", StringComparison.Ordinal)) + { + line = lines[i]; + region.TheirsContent.Add(line); + oursLines.Add(new()); + theirsLines.Add(new(Models.ConflictLineType.Theirs, line, theirsLineNumber++)); + i++; + } + + // Capture end marker (e.g., ">>>>>>> feature-branch") + if (i < lines.Length && lines[i].StartsWith(">>>>>>>", StringComparison.Ordinal)) + { + oursLines.Add(new()); + theirsLines.Add(new()); + + region.EndMarker = lines[i]; + region.EndLineInOriginal = i; + i++; + } + + _conflictRegions.Add(region); + } + else + { + oursLines.Add(new(Models.ConflictLineType.Common, line, oursLineNumber)); + theirsLines.Add(new(Models.ConflictLineType.Common, line, theirsLineNumber)); + i++; + oursLineNumber++; + theirsLineNumber++; + } + } + + MaxLineNumber = Math.Max(oursLineNumber, theirsLineNumber); + OursLines = oursLines; + TheirsLines = theirsLines; + } + + private void RefreshDisplayData() + { + var resultLines = new List(); + _lineStates.Clear(); + + if (_oursLines == null || _oursLines.Count == 0) + { + ResultLines = resultLines; + return; + } + + int resultLineNumber = 1; + int currentLine = 0; + int conflictIdx = 0; + + while (currentLine < _oursLines.Count) + { + // Check if we're at a conflict region + Models.ConflictRegion currentRegion = null; + if (conflictIdx < _conflictRegions.Count) + { + var region = _conflictRegions[conflictIdx]; + if (region.StartLineInOriginal == currentLine) + currentRegion = region; + } + + if (currentRegion != null) + { + int regionLines = currentRegion.EndLineInOriginal - currentRegion.StartLineInOriginal + 1; + if (currentRegion.IsResolved) + { + var oldLineCount = resultLines.Count; + var resolveType = currentRegion.ResolutionType; + + // Resolved - show resolved content with color based on resolution type + if (resolveType == Models.ConflictResolution.UseBothMineFirst) + { + int mineCount = currentRegion.OursContent.Count; + for (int i = 0; i < mineCount; i++) + { + resultLines.Add(new(Models.ConflictLineType.Ours, currentRegion.OursContent[i], resultLineNumber)); + resultLineNumber++; + } + + int theirsCount = currentRegion.TheirsContent.Count; + for (int i = 0; i < theirsCount; i++) + { + resultLines.Add(new(Models.ConflictLineType.Theirs, currentRegion.TheirsContent[i], resultLineNumber)); + resultLineNumber++; + } + } + else if (resolveType == Models.ConflictResolution.UseBothTheirsFirst) + { + int theirsCount = currentRegion.TheirsContent.Count; + for (int i = 0; i < theirsCount; i++) + { + resultLines.Add(new(Models.ConflictLineType.Theirs, currentRegion.TheirsContent[i], resultLineNumber)); + resultLineNumber++; + } + + int mineCount = currentRegion.OursContent.Count; + for (int i = 0; i < mineCount; i++) + { + resultLines.Add(new(Models.ConflictLineType.Ours, currentRegion.OursContent[i], resultLineNumber)); + resultLineNumber++; + } + } + else if (resolveType == Models.ConflictResolution.UseOurs) + { + int mineCount = currentRegion.OursContent.Count; + for (int i = 0; i < mineCount; i++) + { + resultLines.Add(new(Models.ConflictLineType.Ours, currentRegion.OursContent[i], resultLineNumber)); + resultLineNumber++; + } + } + else if (resolveType == Models.ConflictResolution.UseTheirs) + { + int theirsCount = currentRegion.TheirsContent.Count; + for (int i = 0; i < theirsCount; i++) + { + resultLines.Add(new(Models.ConflictLineType.Theirs, currentRegion.TheirsContent[i], resultLineNumber)); + resultLineNumber++; + } + } + + // Pad with empty lines to match Mine/Theirs panel height + int added = resultLines.Count - oldLineCount; + int padding = regionLines - added; + for (int p = 0; p < padding; p++) + resultLines.Add(new()); + + int blockSize = resultLines.Count - oldLineCount - 2; + _lineStates.Add(Models.ConflictLineState.ResolvedBlockStart); + for (var i = 0; i < blockSize; i++) + _lineStates.Add(Models.ConflictLineState.ResolvedBlock); + _lineStates.Add(Models.ConflictLineState.ResolvedBlockEnd); + } + else + { + resultLines.Add(new(Models.ConflictLineType.Marker, currentRegion.StartMarker)); + _lineStates.Add(Models.ConflictLineState.ConflictBlockStart); + + foreach (var line in currentRegion.OursContent) + { + resultLines.Add(new(Models.ConflictLineType.Ours, line, resultLineNumber++)); + _lineStates.Add(Models.ConflictLineState.ConflictBlock); + } + + resultLines.Add(new(Models.ConflictLineType.Marker, currentRegion.SeparatorMarker)); + _lineStates.Add(Models.ConflictLineState.ConflictBlock); + + foreach (var line in currentRegion.TheirsContent) + { + resultLines.Add(new(Models.ConflictLineType.Theirs, line, resultLineNumber++)); + _lineStates.Add(Models.ConflictLineState.ConflictBlock); + } + + resultLines.Add(new(Models.ConflictLineType.Marker, currentRegion.EndMarker)); + _lineStates.Add(Models.ConflictLineState.ConflictBlockEnd); + } + + currentLine = currentRegion.EndLineInOriginal + 1; + conflictIdx++; + } + else + { + var oursLine = _oursLines[currentLine]; + resultLines.Add(new(oursLine.Type, oursLine.Content, resultLineNumber)); + _lineStates.Add(Models.ConflictLineState.Normal); + resultLineNumber++; + currentLine++; + } + } + + SelectedChunk = null; + ResultLines = resultLines; + + var unsolved = new List(); + for (var i = 0; i < _conflictRegions.Count; i++) + { + var r = _conflictRegions[i]; + if (!r.IsResolved) + unsolved.Add(i); + } + + UnsolvedCount = unsolved.Count; + } + + private readonly Repository _repo; + private readonly string _filePath; + private string _originalContent = string.Empty; + private int _unsolvedCount = 0; + private int _maxLineNumber = 0; + private List _oursLines = []; + private List _theirsLines = []; + private List _resultLines = []; + private List _conflictRegions = []; + private List _lineStates = []; + private Vector _scrollOffset = Vector.Zero; + private Models.ConflictSelectedChunk _selectedChunk; + private string _error = string.Empty; + } +} diff --git a/src/ViewModels/MoveRepositoryNode.cs b/src/ViewModels/MoveRepositoryNode.cs index 0c213058b..716274295 100644 --- a/src/ViewModels/MoveRepositoryNode.cs +++ b/src/ViewModels/MoveRepositoryNode.cs @@ -32,6 +32,7 @@ public MoveRepositoryNode(RepositoryNode target) Id = Guid.NewGuid().ToString() }); MakeRows(Preferences.Instance.RepositoryNodes, 1); + Selected = Rows[0]; } public override Task Sure() diff --git a/src/ViewModels/OpenFileCommandPalette.cs b/src/ViewModels/OpenFileCommandPalette.cs index 2ca5b706b..78a702e0d 100644 --- a/src/ViewModels/OpenFileCommandPalette.cs +++ b/src/ViewModels/OpenFileCommandPalette.cs @@ -35,9 +35,8 @@ public string SelectedFile set => SetProperty(ref _selectedFile, value); } - public OpenFileCommandPalette(Launcher launcher, string repo) + public OpenFileCommandPalette(string repo) { - _launcher = launcher; _repo = repo; _isLoading = true; @@ -56,16 +55,6 @@ public OpenFileCommandPalette(Launcher launcher, string repo) }); } - public override void Cleanup() - { - _launcher = null; - _repo = null; - _repoFiles.Clear(); - _filter = null; - _visibleFiles.Clear(); - _selectedFile = null; - } - public void ClearFilter() { Filter = string.Empty; @@ -73,10 +62,12 @@ public void ClearFilter() public void Launch() { + _repoFiles.Clear(); + _visibleFiles.Clear(); + Close(); + if (!string.IsNullOrEmpty(_selectedFile)) Native.OS.OpenWithDefaultEditor(Native.OS.GetAbsPath(_repo, _selectedFile)); - - _launcher.CancelCommandPalette(); } private void UpdateVisible() @@ -112,7 +103,6 @@ private void UpdateVisible() } } - private Launcher _launcher = null; private string _repo = null; private bool _isLoading = false; private List _repoFiles = null; diff --git a/src/ViewModels/OpenLocalRepository.cs b/src/ViewModels/OpenLocalRepository.cs new file mode 100644 index 000000000..877818d92 --- /dev/null +++ b/src/ViewModels/OpenLocalRepository.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class OpenLocalRepository : Popup + { + [Required(ErrorMessage = "Repository folder is required")] + [CustomValidation(typeof(OpenLocalRepository), nameof(ValidateRepoPath))] + public string RepoPath + { + get => _repoPath; + set => SetProperty(ref _repoPath, value, true); + } + + public List Groups + { + get; + } + + public RepositoryNode Group + { + get => _group; + set => SetProperty(ref _group, value); + } + + public int Bookmark + { + get => _bookmark; + set => SetProperty(ref _bookmark, value); + } + + public OpenLocalRepository(string pageId, RepositoryNode group) + { + _pageId = pageId; + + Groups = new List(); + Groups.Add(new RepositoryNode { Name = "No Group (Uncategorized)", Id = string.Empty }); + Group = group ?? Groups[0]; + CollectGroups(Groups, Preferences.Instance.RepositoryNodes); + } + + public static ValidationResult ValidateRepoPath(string folder, ValidationContext _) + { + if (!Directory.Exists(folder)) + return new ValidationResult("Given path can NOT be found"); + return ValidationResult.Success; + } + + public override async Task Sure() + { + var isBare = await new Commands.IsBareRepository(_repoPath).GetResultAsync(); + var parent = _group is { Id: not "" } ? _group : null; + var repoRoot = _repoPath; + if (!isBare) + { + var test = await new Commands.QueryRepositoryRootPath(_repoPath).GetResultAsync(); + if (test.IsSuccess && !string.IsNullOrWhiteSpace(test.StdOut)) + { + repoRoot = test.StdOut.Trim(); + } + else + { + var launcher = App.GetLauncher(); + foreach (var page in launcher.Pages) + { + if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + { + page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr); + break; + } + } + + return false; + } + } + + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(repoRoot, parent, true); + node.Bookmark = _bookmark; + await node.UpdateStatusAsync(false, null); + Welcome.Instance.Refresh(); + node.Open(); + return true; + } + + private void CollectGroups(List outs, List collections) + { + foreach (var node in collections) + { + if (!node.IsRepository) + { + outs.Add(node); + CollectGroups(outs, node.SubNodes); + } + } + } + + private string _pageId = string.Empty; + private string _repoPath = string.Empty; + private RepositoryNode _group = null; + private int _bookmark = 0; + } +} diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 564d415b6..25bbb431f 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading.Tasks; using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; @@ -104,6 +105,12 @@ public int EditorTabWidth set => SetProperty(ref _editorTabWidth, value); } + public double Zoom + { + get => _zoom; + set => SetProperty(ref _zoom, value); + } + public LayoutInfo Layout { get => _layout; @@ -149,6 +156,19 @@ public int DateTimeFormat } } + public bool Use24Hours + { + get => Models.DateTimeFormat.Use24Hours; + set + { + if (value != Models.DateTimeFormat.Use24Hours) + { + Models.DateTimeFormat.Use24Hours = value; + OnPropertyChanged(); + } + } + } + public bool UseFixedTabWidth { get => _useFixedTabWidth; @@ -173,41 +193,23 @@ public bool Check4UpdatesOnStartup set => SetProperty(ref _check4UpdatesOnStartup, value); } - public bool ShowAuthorTimeInGraph - { - get => _showAuthorTimeInGraph; - set => SetProperty(ref _showAuthorTimeInGraph, value); - } - - public bool ShowChildren - { - get => _showChildren; - set => SetProperty(ref _showChildren, value); - } - public string IgnoreUpdateTag { get => _ignoreUpdateTag; set => SetProperty(ref _ignoreUpdateTag, value); } - public bool ShowTagsAsTree - { - get; - set; - } = false; - public bool ShowTagsInGraph { get => _showTagsInGraph; set => SetProperty(ref _showTagsInGraph, value); } - public bool ShowSubmodulesAsTree + public bool UseCompactBranchNamesInGraph { - get; - set; - } = false; + get => _useCompactBranchNamesInGraph; + set => SetProperty(ref _useCompactBranchNamesInGraph, value); + } public bool UseTwoColumnsLayoutInHistories { @@ -235,17 +237,28 @@ public bool UseSyntaxHighlighting public bool IgnoreCRAtEOLInDiff { - get => Models.DiffOption.IgnoreCRAtEOL; - set - { - if (Models.DiffOption.IgnoreCRAtEOL != value) - { - Models.DiffOption.IgnoreCRAtEOL = value; - OnPropertyChanged(); - } - } + get => _ignoreCRAtEOLInDiff; + set => SetProperty(ref _ignoreCRAtEOLInDiff, value); } + public bool UseStashAndReapplyByDefault + { + get; + set; + } = false; + + public bool EnableAutoFetch + { + get; + set; + } = false; + + public int AutoFetchInterval + { + get; + set; + } = 10; + public bool IgnoreWhitespaceChangesInDiff { get => _ignoreWhitespaceChangesInDiff; @@ -449,12 +462,6 @@ public string ExternalMergeToolMergeArgs } } - public uint StatisticsSampleColor - { - get => _statisticsSampleColor; - set => SetProperty(ref _statisticsSampleColor, value); - } - public List RepositoryNodes { get; @@ -473,7 +480,7 @@ public AvaloniaList CustomActions set; } = []; - public AvaloniaList OpenAIServices + public AvaloniaList OpenAIServices { get; set; @@ -609,14 +616,35 @@ public void AutoRemoveInvalidNode() RemoveInvalidRepositoriesRecursive(RepositoryNodes); } + public void UpdateAvailableAIModels() + { + Task.Run(() => + { + foreach (var service in OpenAIServices) + { + try + { + service.FetchAvailableModels(); + } + catch + { + // Ignore errors. + } + } + }); + } + public void Save() { if (_isLoading || _isReadonly) return; - var file = Path.Combine(Native.OS.DataDir, "preference.json"); - using var stream = File.Create(file); - JsonSerializer.Serialize(stream, this, JsonCodeGen.Default.Preferences); + var tmpfile = Path.Combine(Native.OS.DataDir, "preference_tmp.json"); + var content = JsonSerializer.Serialize(this, JsonCodeGen.Default.Preferences); + File.WriteAllText(tmpfile, content); + + var finalFile = Path.Combine(Native.OS.DataDir, "preference.json"); + File.Move(tmpfile, finalFile, true); } private static Preferences Load() @@ -770,6 +798,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private double _defaultFontSize = 13; private double _editorFontSize = 13; private int _editorTabWidth = 4; + private double _zoom = 1.0; private LayoutInfo _layout = new(); private int _maxHistoryCommits = 20000; @@ -777,8 +806,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private bool _useFixedTabWidth = true; private bool _useAutoHideScrollBars = true; private bool _useGitHubStyleAvatar = true; - private bool _showAuthorTimeInGraph = false; - private bool _showChildren = false; + private bool _useCompactBranchNamesInGraph = true; private bool _check4UpdatesOnStartup = true; private double _lastCheckUpdateTime = 0; @@ -789,6 +817,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private bool _displayTimeAsPeriodInHistories = false; private bool _useSideBySideDiff = false; private bool _ignoreWhitespaceChangesInDiff = false; + private bool _ignoreCRAtEOLInDiff = true; private bool _useSyntaxHighlighting = false; private bool _enableDiffViewWordWrap = false; private bool _showHiddenSymbolsInDiffView = false; @@ -804,6 +833,5 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private string _gitDefaultCloneDir = string.Empty; private int _shellOrTerminalType = -1; - private uint _statisticsSampleColor = 0xFF00FF00; } } diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 378d63f40..367bca40d 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -38,27 +38,21 @@ public Models.Branch SelectedBranch set => SetProperty(ref _selectedBranch, value, true); } - public bool DiscardLocalChanges + public bool HasLocalChanges { - get; - set; - } = false; - - public bool UseRebase - { - get => _repo.Settings.PreferRebaseInsteadOfMerge; - set => _repo.Settings.PreferRebaseInsteadOfMerge = value; + get => _repo.LocalChangesCount > 0; } - public bool IsRecurseSubmoduleVisible + public Models.DealWithLocalChanges DealWithLocalChanges { - get => _repo.Submodules.Count > 0; + get; + set; } - public bool RecurseSubmodules + public bool UseRebase { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; + get => _repo.UIStates.PreferRebaseInsteadOfMerge; + set => _repo.UIStates.PreferRebaseInsteadOfMerge = value; } public Pull(Repository repo, Models.Branch specifiedRemoteBranch) @@ -66,6 +60,10 @@ public Pull(Repository repo, Models.Branch specifiedRemoteBranch) _repo = repo; Current = repo.CurrentBranch; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; + if (specifiedRemoteBranch != null) { _selectedRemote = repo.Remotes.Find(x => x.Name == specifiedRemoteBranch.Remote); @@ -118,26 +116,30 @@ public override async Task Sure() var log = _repo.CreateLog("Pull"); Use(log); - var updateSubmodules = IsRecurseSubmoduleVisible && RecurseSubmodules; var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); var needPopStash = false; if (changes > 0) { - if (DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) { - await Commands.Discard.AllAsync(_repo.FullPath, false, false, log); + // Do nothing, just let the pull command fail and show the error to user } - else + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { - var succ = await new Commands.Stash(_repo.FullPath).Use(log).PushAsync("PULL_AUTO_STASH"); + var succ = await new Commands.Stash(_repo.FullPath).Use(log).PushAsync("PULL_AUTO_STASH", false); if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } + else + { + await Commands.Discard.AllAsync(_repo.FullPath, true, false, false, log); + } } bool rs = await new Commands.Pull( @@ -147,12 +149,7 @@ 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); - } + await _repo.AutoUpdateSubmodulesAsync(log); if (needPopStash) await new Commands.Stash(_repo.FullPath).Use(log).PopAsync("stash@{0}"); diff --git a/src/ViewModels/Push.cs b/src/ViewModels/Push.cs index 10999f259..b4d9029a9 100644 --- a/src/ViewModels/Push.cs +++ b/src/ViewModels/Push.cs @@ -58,7 +58,7 @@ public Models.Branch SelectedRemoteBranch set { if (SetProperty(ref _selectedRemoteBranch, value, true)) - IsSetTrackOptionVisible = value != null && _selectedLocalBranch.Upstream != value.FullName; + IsSetTrackOptionVisible = value != null && (value.Head == null || _selectedLocalBranch.Upstream != value.FullName); } } @@ -87,8 +87,8 @@ public bool CheckSubmodules public bool PushAllTags { - get => _repo.Settings.PushAllTags; - set => _repo.Settings.PushAllTags = value; + get => _repo.UIStates.PushAllTags; + set => _repo.UIStates.PushAllTags = value; } public bool ForcePush @@ -212,11 +212,14 @@ public override async Task Sure() private void AutoSelectBranchByRemote() { + if (_selectedRemote == null || _selectedLocalBranch == null) + return; + // Gather branches. var branches = new List(); foreach (var branch in _repo.Branches) { - if (branch.Remote == _selectedRemote.Name) + if (!branch.IsLocal && _selectedRemote.Name.Equals(branch.Remote, StringComparison.Ordinal)) branches.Add(branch); } @@ -225,7 +228,7 @@ private void AutoSelectBranchByRemote() { foreach (var branch in branches) { - if (_selectedLocalBranch.Upstream == branch.FullName) + if (_selectedLocalBranch.Upstream.Equals(branch.FullName, StringComparison.Ordinal)) { RemoteBranches = branches; SelectedRemoteBranch = branch; @@ -237,7 +240,7 @@ private void AutoSelectBranchByRemote() // Try to find a remote branch with the same name of selected local branch. foreach (var branch in branches) { - if (_selectedLocalBranch.Name == branch.Name) + if (_selectedLocalBranch.Name.Equals(branch.Name, StringComparison.Ordinal)) { RemoteBranches = branches; SelectedRemoteBranch = branch; diff --git a/src/ViewModels/Rebase.cs b/src/ViewModels/Rebase.cs index 315e3c634..2213a55af 100644 --- a/src/ViewModels/Rebase.cs +++ b/src/ViewModels/Rebase.cs @@ -1,7 +1,18 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using Avalonia.Threading; namespace SourceGit.ViewModels { + public enum RebaseTestingState + { + Disabled = 0, + Testing, + WillCauseConflicts, + UnknownError, + NoConflicts, + } + public class Rebase : Popup { public Models.Branch Current @@ -22,6 +33,18 @@ public bool AutoStash set; } + public bool NoVerify + { + get; + set; + } + + public RebaseTestingState TestingState + { + get => _testingState; + private set => SetProperty(ref _testingState, value); + } + public Rebase(Repository repo, Models.Branch current, Models.Branch on) { _repo = repo; @@ -29,6 +52,8 @@ public Rebase(Repository repo, Models.Branch current, Models.Branch on) Current = current; On = on; AutoStash = true; + + Test(); } public Rebase(Repository repo, Models.Branch current, Models.Commit on) @@ -38,6 +63,8 @@ public Rebase(Repository repo, Models.Branch current, Models.Commit on) Current = current; On = on; AutoStash = true; + + Test(); } public override async Task Sure() @@ -49,7 +76,7 @@ public override async Task Sure() var log = _repo.CreateLog("Rebase"); Use(log); - await new Commands.Rebase(_repo.FullPath, _revision, AutoStash) + await new Commands.Rebase(_repo.FullPath, _revision, AutoStash, NoVerify) .Use(log) .ExecAsync(); @@ -57,7 +84,45 @@ public override async Task Sure() return true; } + private void Test() + { + if (Native.OS.GitVersion < Models.GitVersions.REPLAY) + return; + + var head = Current.Head; + TestingState = RebaseTestingState.Testing; + Task.Run(async () => + { + var mergeBase = await new Commands.MergeBase(_repo.FullPath, head, _revision) + .GetResultAsync() + .ConfigureAwait(false); + + if (string.IsNullOrEmpty(mergeBase)) + { + Dispatcher.UIThread.Post(() => TestingState = RebaseTestingState.UnknownError); + return; + } + else if (head.Equals(mergeBase, StringComparison.Ordinal)) + { + Dispatcher.UIThread.Post(() => TestingState = RebaseTestingState.NoConflicts); + return; + } + + var exitCode = await new Commands.Replay(_repo.FullPath, _revision, $"{mergeBase}..{head}") + .GetExitCodeAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => TestingState = exitCode switch + { + 0 => RebaseTestingState.NoConflicts, + 1 => RebaseTestingState.WillCauseConflicts, + _ => RebaseTestingState.UnknownError, + }); + }); + } + private readonly Repository _repo; private readonly string _revision; + private RebaseTestingState _testingState = RebaseTestingState.Disabled; } } diff --git a/src/ViewModels/RemoveWorktree.cs b/src/ViewModels/RemoveWorktree.cs index 249c7683b..40c3b87b3 100644 --- a/src/ViewModels/RemoveWorktree.cs +++ b/src/ViewModels/RemoveWorktree.cs @@ -4,7 +4,7 @@ namespace SourceGit.ViewModels { public class RemoveWorktree : Popup { - public Models.Worktree Target + public Worktree Target { get; } @@ -15,7 +15,7 @@ public bool Force set; } = false; - public RemoveWorktree(Repository repo, Models.Worktree target) + public RemoveWorktree(Repository repo, Worktree target) { _repo = repo; Target = target; diff --git a/src/ViewModels/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index ececb16a5..dbca651e7 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -60,27 +60,9 @@ public override async Task Sure() .RenameAsync(_name); if (succ) - { - foreach (var filter in _repo.HistoryFilterCollection.Filters) - { - if (filter.Type == Models.FilterType.LocalBranch && - filter.Pattern.Equals(oldName, StringComparison.Ordinal)) - { - filter.Pattern = $"refs/heads/{_name}"; - break; - } - } - } + _repo.RefreshAfterRenameBranch(Target, _name); log.Complete(); - _repo.MarkBranchesDirtyManually(); - - if (isCurrent) - { - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); - } - return succ; } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 3f8c26ee8..99a3a47e2 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -35,23 +34,36 @@ public Models.RepositorySettings Settings get => _settings; } + public Models.RepositoryUIStates UIStates + { + get => _uiStates; + } + public Models.GitFlow GitFlow { get; set; } = new(); - public Models.HistoryFilterCollection HistoryFilterCollection - { - get => _historyFilterCollection; - } - public Models.FilterMode HistoryFilterMode { get => _historyFilterMode; private set => SetProperty(ref _historyFilterMode, value); } + public bool IsHistoryFiltersCollapsed + { + get => _uiStates.IsHistoryFiltersCollapsed; + set + { + if (value != _uiStates.IsHistoryFiltersCollapsed) + { + _uiStates.IsHistoryFiltersCollapsed = value; + OnPropertyChanged(); + } + } + } + public bool HasAllowedSignersFile { get => _hasAllowedSignersFile; @@ -64,30 +76,51 @@ public int SelectedViewIndex { if (SetProperty(ref _selectedViewIndex, value)) { - SelectedView = value switch - { - 1 => _workingCopy, - 2 => _stashesPage, - _ => _histories, - }; + OnPropertyChanged(nameof(IsHistoriesVisible)); + OnPropertyChanged(nameof(IsWorkingCopyVisible)); + OnPropertyChanged(nameof(IsStashesVisible)); } } } - public object SelectedView + public Histories Histories + { + get => _histories; + } + + public WorkingCopy WorkingCopy + { + get => _workingCopy; + } + + public StashesPage StashesPage + { + get => _stashesPage; + } + + public bool IsHistoriesVisible + { + get => SelectedViewIndex == 0; + } + + public bool IsWorkingCopyVisible + { + get => SelectedViewIndex == 1; + } + + public bool IsStashesVisible { - get => _selectedView; - set => SetProperty(ref _selectedView, value); + get => SelectedViewIndex == 2; } - public bool EnableTopoOrderInHistories + public bool EnableTopoOrderInHistory { - get => _settings.EnableTopoOrderInHistories; + get => _uiStates.EnableTopoOrderInHistory; set { - if (value != _settings.EnableTopoOrderInHistories) + if (value != _uiStates.EnableTopoOrderInHistory) { - _settings.EnableTopoOrderInHistories = value; + _uiStates.EnableTopoOrderInHistory = value; RefreshCommits(); } } @@ -95,30 +128,17 @@ public bool EnableTopoOrderInHistories public Models.HistoryShowFlags HistoryShowFlags { - get => _settings.HistoryShowFlags; + get => _uiStates.HistoryShowFlags; private set { - if (value != _settings.HistoryShowFlags) + if (value != _uiStates.HistoryShowFlags) { - _settings.HistoryShowFlags = value; + _uiStates.HistoryShowFlags = value; RefreshCommits(); } } } - public bool OnlyHighlightCurrentBranchInHistories - { - get => _settings.OnlyHighlightCurrentBranchInHistories; - set - { - if (value != _settings.OnlyHighlightCurrentBranchInHistories) - { - _settings.OnlyHighlightCurrentBranchInHistories = value; - OnPropertyChanged(); - } - } - } - public string Filter { get => _filter; @@ -153,9 +173,10 @@ public Models.Branch CurrentBranch private set { var oldHead = _currentBranch?.Head; - if (SetProperty(ref _currentBranch, value) && value != null) + if (SetProperty(ref _currentBranch, value)) { - if (oldHead != _currentBranch.Head && _workingCopy is { UseAmend: true }) + _histories?.NotifyCurrentBranchChanged(); + if (value != null && !value.Head.Equals(oldHead, StringComparison.Ordinal) && _workingCopy is { UseAmend: true }) _workingCopy.UseAmend = false; } } @@ -173,7 +194,7 @@ public List RemoteBranchTrees private set => SetProperty(ref _remoteBranchTrees, value); } - public List Worktrees + public List Worktrees { get => _worktrees; private set => SetProperty(ref _worktrees, value); @@ -187,12 +208,12 @@ public List Tags public bool ShowTagsAsTree { - get => Preferences.Instance.ShowTagsAsTree; + get => _uiStates.ShowTagsAsTree; set { - if (value != Preferences.Instance.ShowTagsAsTree) + if (value != _uiStates.ShowTagsAsTree) { - Preferences.Instance.ShowTagsAsTree = value; + _uiStates.ShowTagsAsTree = value; VisibleTags = BuildVisibleTags(); OnPropertyChanged(); } @@ -213,12 +234,12 @@ public List Submodules public bool ShowSubmodulesAsTree { - get => Preferences.Instance.ShowSubmodulesAsTree; + get => _uiStates.ShowSubmodulesAsTree; set { - if (value != Preferences.Instance.ShowSubmodulesAsTree) + if (value != _uiStates.ShowSubmodulesAsTree) { - Preferences.Instance.ShowSubmodulesAsTree = value; + _uiStates.ShowSubmodulesAsTree = value; VisibleSubmodules = BuildVisibleSubmodules(); OnPropertyChanged(); } @@ -251,12 +272,12 @@ public int LocalBranchesCount public bool IncludeUntracked { - get => _settings.IncludeUntrackedInLocalChanges; + get => _uiStates.IncludeUntrackedInLocalChanges; set { - if (value != _settings.IncludeUntrackedInLocalChanges) + if (value != _uiStates.IncludeUntrackedInLocalChanges) { - _settings.IncludeUntrackedInLocalChanges = value; + _uiStates.IncludeUntrackedInLocalChanges = value; OnPropertyChanged(); RefreshWorkingCopyChanges(); } @@ -285,12 +306,12 @@ public SearchCommitContext SearchCommitContext public bool IsLocalBranchGroupExpanded { - get => _settings.IsLocalBranchesExpandedInSideBar; + get => _uiStates.IsLocalBranchesExpandedInSideBar; set { - if (value != _settings.IsLocalBranchesExpandedInSideBar) + if (value != _uiStates.IsLocalBranchesExpandedInSideBar) { - _settings.IsLocalBranchesExpandedInSideBar = value; + _uiStates.IsLocalBranchesExpandedInSideBar = value; OnPropertyChanged(); } } @@ -298,12 +319,12 @@ public bool IsLocalBranchGroupExpanded public bool IsRemoteGroupExpanded { - get => _settings.IsRemotesExpandedInSideBar; + get => _uiStates.IsRemotesExpandedInSideBar; set { - if (value != _settings.IsRemotesExpandedInSideBar) + if (value != _uiStates.IsRemotesExpandedInSideBar) { - _settings.IsRemotesExpandedInSideBar = value; + _uiStates.IsRemotesExpandedInSideBar = value; OnPropertyChanged(); } } @@ -311,12 +332,12 @@ public bool IsRemoteGroupExpanded public bool IsTagGroupExpanded { - get => _settings.IsTagsExpandedInSideBar; + get => _uiStates.IsTagsExpandedInSideBar; set { - if (value != _settings.IsTagsExpandedInSideBar) + if (value != _uiStates.IsTagsExpandedInSideBar) { - _settings.IsTagsExpandedInSideBar = value; + _uiStates.IsTagsExpandedInSideBar = value; OnPropertyChanged(); } } @@ -324,12 +345,12 @@ public bool IsTagGroupExpanded public bool IsSubmoduleGroupExpanded { - get => _settings.IsSubmodulesExpandedInSideBar; + get => _uiStates.IsSubmodulesExpandedInSideBar; set { - if (value != _settings.IsSubmodulesExpandedInSideBar) + if (value != _uiStates.IsSubmodulesExpandedInSideBar) { - _settings.IsSubmodulesExpandedInSideBar = value; + _uiStates.IsSubmodulesExpandedInSideBar = value; OnPropertyChanged(); } } @@ -337,12 +358,12 @@ public bool IsSubmoduleGroupExpanded public bool IsWorktreeGroupExpanded { - get => _settings.IsWorktreeExpandedInSideBar; + get => _uiStates.IsWorktreeExpandedInSideBar; set { - if (value != _settings.IsWorktreeExpandedInSideBar) + if (value != _uiStates.IsWorktreeExpandedInSideBar) { - _settings.IsWorktreeExpandedInSideBar = value; + _uiStates.IsWorktreeExpandedInSideBar = value; OnPropertyChanged(); } } @@ -350,10 +371,10 @@ public bool IsWorktreeGroupExpanded public bool IsSortingLocalBranchByName { - get => _settings.LocalBranchSortMode == Models.BranchSortMode.Name; + get => _uiStates.LocalBranchSortMode == Models.BranchSortMode.Name; set { - _settings.LocalBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; + _uiStates.LocalBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; OnPropertyChanged(); var builder = BuildBranchTree(_branches, _remotes); @@ -364,10 +385,10 @@ public bool IsSortingLocalBranchByName public bool IsSortingRemoteBranchByName { - get => _settings.RemoteBranchSortMode == Models.BranchSortMode.Name; + get => _uiStates.RemoteBranchSortMode == Models.BranchSortMode.Name; set { - _settings.RemoteBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; + _uiStates.RemoteBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; OnPropertyChanged(); var builder = BuildBranchTree(_branches, _remotes); @@ -378,10 +399,10 @@ public bool IsSortingRemoteBranchByName public bool IsSortingTagsByName { - get => _settings.TagSortMode == Models.TagSortMode.Name; + get => _uiStates.TagSortMode == Models.TagSortMode.Name; set { - _settings.TagSortMode = value ? Models.TagSortMode.Name : Models.TagSortMode.CreatorDate; + _uiStates.TagSortMode = value ? Models.TagSortMode.Name : Models.TagSortMode.CreatorDate; OnPropertyChanged(); VisibleTags = BuildVisibleTags(); } @@ -427,87 +448,45 @@ public Repository(bool isBare, string path, string gitDir) GitDir = gitDir.Replace('\\', '/').TrimEnd('/'); var commonDirFile = Path.Combine(GitDir, "commondir"); - _isWorktree = GitDir.IndexOf("/worktrees/", StringComparison.Ordinal) > 0 && + var isWorktree = GitDir.IndexOf("/worktrees/", StringComparison.Ordinal) > 0 && File.Exists(commonDirFile); - if (_isWorktree) + if (isWorktree) { var commonDir = File.ReadAllText(commonDirFile).Trim(); - if (!Path.IsPathRooted(commonDir)) + if (Path.IsPathRooted(commonDir)) + commonDir = new DirectoryInfo(commonDir).FullName; + else commonDir = new DirectoryInfo(Path.Combine(GitDir, commonDir)).FullName; - _gitCommonDir = commonDir; + _gitCommonDir = commonDir.Replace('\\', '/').TrimEnd('/'); } else { _gitCommonDir = GitDir; } + + _settings = Models.RepositorySettings.Get(_gitCommonDir); + _uiStates = Models.RepositoryUIStates.Load(GitDir); } public void Open() { - var settingsFile = Path.Combine(_gitCommonDir, "sourcegit.settings"); - if (File.Exists(settingsFile)) - { - try - { - using var stream = File.OpenRead(settingsFile); - _settings = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.RepositorySettings); - } - catch - { - _settings = new Models.RepositorySettings(); - } - } - else - { - _settings = new Models.RepositorySettings(); - } - - var historyFilterFile = Path.Combine(GitDir, "sourcegit.filters"); - if (File.Exists(historyFilterFile)) - { - try - { - using var stream = File.OpenRead(historyFilterFile); - _historyFilterCollection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.HistoryFilterCollection); - } - catch - { - _historyFilterCollection = new Models.HistoryFilterCollection(); - } - } - else - { - _historyFilterCollection = new Models.HistoryFilterCollection(); - } - 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}"); + SendNotification($"Failed to start watcher for repository: '{FullPath}'. You may need to press 'F5' to refresh repository manually!\n\nReason: {ex.Message}", true); } - _historyFilterMode = _historyFilterCollection.Mode; + _historyFilterMode = _uiStates.GetHistoryFilterMode(); _histories = new Histories(this); - _workingCopy = new WorkingCopy(this) { CommitMessage = _settings.LastCommitMessage }; + _workingCopy = new WorkingCopy(this) { CommitMessage = _uiStates.LastCommitMessage }; _stashesPage = new StashesPage(this); _searchCommitContext = new SearchCommitContext(this); - - if (Preferences.Instance.ShowLocalChangesByDefault) - { - _selectedView = _workingCopy; - _selectedViewIndex = 1; - } - else - { - _selectedView = _histories; - _selectedViewIndex = 0; - } - + _selectedViewIndex = Preferences.Instance.ShowLocalChangesByDefault ? 1 : 0; _lastFetchTime = DateTime.Now; _autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000); RefreshAll(); @@ -515,39 +494,12 @@ public void Open() public void Close() { - SelectedView = null; // Do NOT modify. Used to remove exists widgets for GC.Collect - Logs.Clear(); - - if (!_isWorktree && Directory.Exists(_gitCommonDir)) - { - 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 - } - } + var commitMessage = _workingCopy.CommitMessage; + if (!string.IsNullOrEmpty(commitMessage) && _workingCopy.InProgressContext != null) + File.WriteAllText(Path.Combine(GitDir, "MERGE_MSG"), commitMessage); - if (Directory.Exists(GitDir)) - { - try - { - using var stream = File.Create(Path.Combine(GitDir, "sourcegit.filters")); - JsonSerializer.Serialize(stream, _historyFilterCollection, JsonCodeGen.Default.HistoryFilterCollection); - } - catch - { - // Ignore - } - } + _uiStates.LastCommitMessage = commitMessage; + _uiStates.Save(); if (_cancellationRefreshBranches is { IsCancellationRequested: false }) _cancellationRefreshBranches.Cancel(); @@ -560,35 +512,13 @@ public void Close() if (_cancellationRefreshStashes is { IsCancellationRequested: false }) _cancellationRefreshStashes.Cancel(); - _autoFetchTimer.Dispose(); - _autoFetchTimer = null; - - _settings = null; - _historyFilterCollection = null; - _historyFilterMode = Models.FilterMode.None; - _watcher?.Dispose(); - _histories.Dispose(); - _workingCopy.Dispose(); - _stashesPage.Dispose(); - _searchCommitContext.Dispose(); - - _watcher = null; - _histories = null; - _workingCopy = null; - _stashesPage = null; - - _localChangesCount = 0; - _stashesCount = 0; + _autoFetchTimer.Dispose(); + } - _remotes.Clear(); - _branches.Clear(); - _localBranchTrees.Clear(); - _remoteBranchTrees.Clear(); - _tags.Clear(); - _visibleTags = null; - _submodules.Clear(); - _visibleSubmodules = null; + public void SendNotification(string message, bool isError = false) + { + Models.Notification.Send(FullPath, message, isError); } public bool CanCreatePopup() @@ -619,8 +549,8 @@ public async Task ShowAndStartPopupAsync(Popup popup) public bool IsGitFlowEnabled() { return GitFlow is { IsValid: true } && - _branches.Find(x => x.IsLocal && x.Name.Equals(GitFlow.Master, StringComparison.Ordinal)) != null && - _branches.Find(x => x.IsLocal && x.Name.Equals(GitFlow.Develop, StringComparison.Ordinal)) != null; + _branches.Find(x => x.IsLocal && x.Name.Equals(GitFlow.ProductionBranch, StringComparison.Ordinal)) != null && + _branches.Find(x => x.IsLocal && x.Name.Equals(GitFlow.DevelopmentBranch, StringComparison.Ordinal)) != null; } public Models.GitFlowBranchType GetGitFlowType(Models.Branch b) @@ -644,8 +574,15 @@ public bool IsLFSEnabled() 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() @@ -653,7 +590,7 @@ 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!"); + SendNotification("LFS enabled successfully!"); log.Complete(); } @@ -666,7 +603,7 @@ public async Task TrackLFSFileAsync(string pattern, bool isFilenameMode) .TrackAsync(pattern, isFilenameMode); if (succ) - App.SendNotification(FullPath, $"Tracking successfully! Pattern: {pattern}"); + SendNotification($"Tracking successfully! Pattern: {pattern}"); log.Complete(); return succ; @@ -680,7 +617,7 @@ public async Task LockLFSFileAsync(string remote, string path) .LockAsync(remote, path); if (succ) - App.SendNotification(FullPath, $"Lock file successfully! File: {path}"); + SendNotification($"Lock file successfully! File: {path}"); log.Complete(); return succ; @@ -694,7 +631,7 @@ public async Task UnlockLFSFileAsync(string remote, string path, bool forc .UnlockAsync(remote, path, force); if (succ && notify) - App.SendNotification(FullPath, $"Unlock file successfully! File: {path}"); + SendNotification($"Unlock file successfully! File: {path}"); log.Complete(); return succ; @@ -729,18 +666,8 @@ public void RefreshAll() }); 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)) - GitFlow.Master = masterName; - if (config.TryGetValue("gitflow.branch.develop", out var developName)) - GitFlow.Develop = developName; - if (config.TryGetValue("gitflow.prefix.feature", out var featurePrefix)) - GitFlow.FeaturePrefix = featurePrefix; - if (config.TryGetValue("gitflow.prefix.release", out var releasePrefix)) - GitFlow.ReleasePrefix = releasePrefix; - if (config.TryGetValue("gitflow.prefix.hotfix", out var hotfixPrefix)) - GitFlow.HotfixPrefix = hotfixPrefix; + _hasAllowedSignersFile = config.TryGetValue("gpg.ssh.allowedsignersfile", out var allowedSignersFile) && !string.IsNullOrEmpty(allowedSignersFile); + GitFlow.Parse(config); }); } @@ -751,7 +678,7 @@ public async Task FetchAsync(bool autoStart) if (_remotes.Count == 0) { - App.RaiseException(FullPath, "No remotes added to this repository!!!"); + SendNotification("No remotes added to this repository!!!", true); return; } @@ -768,13 +695,13 @@ public async Task PullAsync(bool autoStart) if (_remotes.Count == 0) { - App.RaiseException(FullPath, "No remotes added to this repository!!!"); + SendNotification("No remotes added to this repository!!!", true); return; } if (_currentBranch == null) { - App.RaiseException(FullPath, "Can NOT find current branch!!!"); + SendNotification("Can NOT find current branch!!!", true); return; } @@ -792,13 +719,13 @@ public async Task PushAsync(bool autoStart) if (_remotes.Count == 0) { - App.RaiseException(FullPath, "No remotes added to this repository!!!"); + SendNotification("No remotes added to this repository!!!", true); return; } if (_currentBranch == null) { - App.RaiseException(FullPath, "Can NOT find current branch!!!"); + SendNotification("Can NOT find current branch!!!", true); return; } @@ -842,6 +769,140 @@ public IDisposable LockWatcher() return _watcher?.Lock(); } + public void RefreshAfterCreateBranch(Models.Branch created, bool checkout) + { + _watcher?.MarkBranchUpdated(); + _watcher?.MarkWorkingCopyUpdated(); + + _branches.RemoveAll(b => b.IsLocal && b.Name.Equals(created.Name, StringComparison.Ordinal)); + _branches.Add(created); + + if (checkout) + { + if (_currentBranch.IsDetachedHead) + { + _branches.Remove(_currentBranch); + } + else + { + _currentBranch.IsCurrent = false; + _currentBranch.WorktreePath = null; + } + + created.IsCurrent = true; + created.WorktreePath = FullPath; + + var folderEndIdx = created.FullName.LastIndexOf('/'); + if (folderEndIdx > 10) + _uiStates.ExpandedBranchNodesInSideBar.Add(created.FullName.Substring(0, folderEndIdx)); + + if (_historyFilterMode == Models.FilterMode.Included) + SetBranchFilterMode(created, Models.FilterMode.Included, false, false); + + CurrentBranch = created; + } + + var locals = new List(); + var count = 0; + foreach (var b in _branches) + { + if (b.IsLocal) + { + locals.Add(b); + if (!b.IsDetachedHead) + count++; + } + } + + var builder = BuildBranchTree(locals, [], false); + LocalBranchTrees = builder.Locals; + LocalBranchesCount = count; + + RefreshCommits(); + RefreshWorkingCopyChanges(); + RefreshWorktrees(); + } + + public void RefreshAfterCheckoutBranch(Models.Branch checkouted) + { + _watcher?.MarkBranchUpdated(); + _watcher?.MarkWorkingCopyUpdated(); + + if (_currentBranch.IsDetachedHead) + { + _branches.Remove(_currentBranch); + } + else + { + _currentBranch.IsCurrent = false; + _currentBranch.WorktreePath = null; + } + + checkouted.IsCurrent = true; + checkouted.WorktreePath = FullPath; + if (_historyFilterMode == Models.FilterMode.Included) + SetBranchFilterMode(checkouted, Models.FilterMode.Included, false, false); + + List locals = []; + foreach (var b in _branches) + { + if (b.IsLocal) + locals.Add(b); + } + + var builder = BuildBranchTree(locals, [], false); + LocalBranchTrees = builder.Locals; + CurrentBranch = checkouted; + + RefreshCommits(); + RefreshWorkingCopyChanges(); + RefreshWorktrees(); + } + + public void RefreshAfterRenameBranch(Models.Branch b, string newName) + { + _watcher?.MarkBranchUpdated(); + + var newFullName = $"refs/heads/{newName}"; + _uiStates.RenameBranchFilter(b.FullName, newFullName); + + var renamed = new Models.Branch + { + Name = newName, + FullName = newFullName, + CommitterDate = b.CommitterDate, + Head = b.Head, + IsLocal = b.IsLocal, + IsCurrent = b.IsCurrent, + IsDetachedHead = b.IsDetachedHead, + Upstream = b.Upstream, + Ahead = b.Ahead, + Behind = b.Behind, + Remote = b.Remote, + IsUpstreamGone = b.IsUpstreamGone, + WorktreePath = b.WorktreePath, + }; + + _branches.Remove(b); + _branches.Add(renamed); + + if (b.IsCurrent) + CurrentBranch = renamed; + + List locals = []; + foreach (var branch in _branches) + { + if (branch.IsLocal) + locals.Add(branch); + } + + var builder = BuildBranchTree(locals, [], false); + LocalBranchTrees = builder.Locals; + + RefreshCommits(); + RefreshWorktrees(); + } + public void MarkBranchesDirtyManually() { _watcher?.MarkBranchUpdated(); @@ -870,6 +931,12 @@ public void MarkStashesDirtyManually() RefreshStashes(); } + public void MarkSubmodulesDirtyManually() + { + _watcher?.MarkSubmodulesUpdated(); + RefreshSubmodules(); + } + public void MarkFetched() { _lastFetchTime = DateTime.Now; @@ -888,6 +955,12 @@ public void NavigateToCommit(string sha, bool isDelayMode = false) } } + public void SetCommitMessage(string message) + { + if (_workingCopy is not null) + _workingCopy.CommitMessage = message; + } + public void ClearCommitMessage() { if (_workingCopy is not null) @@ -901,7 +974,7 @@ public Models.Commit GetSelectedCommitInHistory() public void ClearHistoryFilters() { - _historyFilterCollection.Filters.Clear(); + _uiStates.HistoryFilters.Clear(); HistoryFilterMode = Models.FilterMode.None; ResetBranchTreeFilterMode(LocalBranchTrees); @@ -912,32 +985,32 @@ public void ClearHistoryFilters() public void RemoveHistoryFilter(Models.HistoryFilter filter) { - if (_historyFilterCollection.Filters.Remove(filter)) + if (_uiStates.HistoryFilters.Remove(filter)) { - HistoryFilterMode = _historyFilterCollection.Mode; + HistoryFilterMode = _uiStates.GetHistoryFilterMode(); RefreshHistoryFilters(true); } } public void UpdateBranchNodeIsExpanded(BranchTreeNode node) { - if (_settings == null || !string.IsNullOrWhiteSpace(_filter)) + if (_uiStates == null || !string.IsNullOrWhiteSpace(_filter)) return; if (node.IsExpanded) { - if (!_settings.ExpandedBranchNodesInSideBar.Contains(node.Path)) - _settings.ExpandedBranchNodesInSideBar.Add(node.Path); + if (!_uiStates.ExpandedBranchNodesInSideBar.Contains(node.Path)) + _uiStates.ExpandedBranchNodesInSideBar.Add(node.Path); } else { - _settings.ExpandedBranchNodesInSideBar.Remove(node.Path); + _uiStates.ExpandedBranchNodesInSideBar.Remove(node.Path); } } public void SetTagFilterMode(Models.Tag tag, Models.FilterMode mode) { - var changed = _historyFilterCollection.Update(tag.Name, Models.FilterType.Tag, mode); + var changed = _uiStates.UpdateHistoryFilters(tag.Name, Models.FilterType.Tag, mode); if (changed) RefreshHistoryFilters(true); } @@ -956,28 +1029,28 @@ public void SetBranchFilterMode(BranchTreeNode node, Models.FilterMode mode, boo if (clearExists) { - _historyFilterCollection.Filters.Clear(); + _uiStates.HistoryFilters.Clear(); HistoryFilterMode = Models.FilterMode.None; } if (node.Backend is Models.Branch branch) { var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; - var changed = _historyFilterCollection.Update(node.Path, type, mode); + var changed = _uiStates.UpdateHistoryFilters(node.Path, type, mode); if (!changed) return; if (isLocal && !string.IsNullOrEmpty(branch.Upstream) && !branch.IsUpstreamGone) - _historyFilterCollection.Update(branch.Upstream, Models.FilterType.RemoteBranch, mode); + _uiStates.UpdateHistoryFilters(branch.Upstream, Models.FilterType.RemoteBranch, mode); } else { var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; - var changed = _historyFilterCollection.Update(node.Path, type, mode); + var changed = _uiStates.UpdateHistoryFilters(node.Path, type, mode); if (!changed) return; - _historyFilterCollection.RemoveBranchFiltersByPrefix(node.Path); + _uiStates.RemoveBranchFiltersByPrefix(node.Path); } var parentType = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; @@ -993,7 +1066,7 @@ public void SetBranchFilterMode(BranchTreeNode node, Models.FilterMode mode, boo if (parent == null) break; - _historyFilterCollection.Update(parent.Path, parentType, Models.FilterMode.None); + _uiStates.UpdateHistoryFilters(parent.Path, parentType, Models.FilterMode.None); cur = parent; } while (true); @@ -1055,9 +1128,9 @@ public async Task ExecBisectCommandAsync(string subcmd) var head = await new Commands.QueryRevisionByRefName(FullPath, "HEAD").GetResultAsync(); if (!succ) - App.RaiseException(FullPath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); + SendNotification(log.Content.Substring(log.Content.IndexOf('\n')).Trim(), true); else if (log.Content.Contains("is the first bad commit")) - App.SendNotification(FullPath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); + SendNotification(log.Content.Substring(log.Content.IndexOf('\n')).Trim()); MarkBranchesDirtyManually(); NavigateToCommit(head, true); @@ -1118,22 +1191,7 @@ public void RefreshWorktrees() Task.Run(async () => { var worktrees = await new Commands.Worktree(FullPath).ReadAllAsync().ConfigureAwait(false); - if (worktrees.Count == 0) - { - Dispatcher.UIThread.Invoke(() => Worktrees = worktrees); - return; - } - - var cleaned = new List(); - foreach (var worktree in worktrees) - { - if (worktree.FullPath.Equals(FullPath, StringComparison.Ordinal) || - worktree.FullPath.Equals(GitDir, StringComparison.Ordinal)) - continue; - - cleaned.Add(worktree); - } - + var cleaned = Worktree.Build(FullPath, worktrees); Dispatcher.UIThread.Invoke(() => Worktrees = cleaned); }); } @@ -1173,30 +1231,13 @@ public void RefreshCommits() 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.FirstParentOnly)) - builder.Append("--first-parent "); - - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) - builder.Append("--simplify-by-decoration "); + builder + .Append('-').Append(Preferences.Instance.MaxHistoryCommits).Append(' ') + .Append(_uiStates.BuildHistoryParams(GitDir)); - var filters = _historyFilterCollection.Build(); - if (string.IsNullOrEmpty(filters)) - builder.Append("--branches --remotes --tags HEAD"); - else - builder.Append(filters); - - var commits = await new Commands.QueryCommits(FullPath, builder.ToString()).GetResultAsync().ConfigureAwait(false); - var graph = Models.CommitGraph.Parse(commits, _settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)); + var commits = await new Commands.QueryCommits(FullPath, builder.ToString()) + .GetResultAsync() + .ConfigureAwait(false); Dispatcher.UIThread.Invoke(() => { @@ -1207,8 +1248,6 @@ public void RefreshCommits() { _histories.IsLoading = false; _histories.Commits = commits; - _histories.Graph = graph; - BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToCommitDelayed)) @@ -1286,24 +1325,22 @@ public void RefreshWorkingCopyChanges() _cancellationRefreshWorkingCopyChanges = new CancellationTokenSource(); var token = _cancellationRefreshWorkingCopyChanges.Token; + var noOptionalLocks = Interlocked.Add(ref _queryLocalChangesTimes, 1) > 1; Task.Run(async () => { - var changes = await new Commands.QueryLocalChanges(FullPath, _settings.IncludeUntrackedInLocalChanges) + var changes = await new Commands.QueryLocalChanges(FullPath, _uiStates.IncludeUntrackedInLocalChanges, noOptionalLocks) .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; + _workingCopy.SetData(changes); LocalChangesCount = changes.Count; OnPropertyChanged(nameof(InProgressContext)); GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0); @@ -1340,7 +1377,7 @@ public void RefreshStashes() public void ToggleHistoryShowFlag(Models.HistoryShowFlags flag) { - if (_settings.HistoryShowFlags.HasFlag(flag)) + if (_uiStates.HistoryShowFlags.HasFlag(flag)) HistoryShowFlags -= flag; else HistoryShowFlags |= flag; @@ -1350,7 +1387,7 @@ public void CreateNewBranch() { if (_currentBranch == null) { - App.RaiseException(FullPath, "Git cannot create a branch before your first commit."); + SendNotification("Git cannot create a branch before your first commit.", true); return; } @@ -1362,7 +1399,7 @@ public async Task CheckoutBranchAsync(Models.Branch branch) { if (branch.IsLocal) { - var worktree = _worktrees.Find(x => x.Branch.Equals(branch.FullName, StringComparison.Ordinal)); + var worktree = _worktrees.Find(x => x.IsAttachedTo(branch)); if (worktree != null) { OpenWorktree(worktree); @@ -1378,10 +1415,10 @@ public async Task CheckoutBranchAsync(Models.Branch branch) if (branch.IsLocal) { - if (_localChangesCount > 0 || _submodules.Count > 0) - ShowPopup(new Checkout(this, branch.Name)); + if (_workingCopy is { CanSwitchBranchDirectly: true }) + await ShowAndStartPopupAsync(new Checkout(this, branch)); else - await ShowAndStartPopupAsync(new Checkout(this, branch.Name)); + ShowPopup(new Checkout(this, branch)); } else { @@ -1411,18 +1448,6 @@ public async Task CheckoutTagAsync(Models.Tag tag) 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()) @@ -1445,7 +1470,7 @@ public void CreateNewTag() { if (_currentBranch == null) { - App.RaiseException(FullPath, "Git cannot create a branch before your first commit."); + SendNotification("Git cannot create a tag before your first commit.", true); return; } @@ -1471,6 +1496,14 @@ public void DeleteRemote(Models.Remote remote) ShowPopup(new DeleteRemote(this, remote)); } + public async Task ToggleAutoFetchOnRemoteAsync(Models.Remote remote) + { + var val = remote.DisableAutoFetch ? "false" : "true"; + var succ = await new Commands.Config(FullPath).SetAsync($"remote.{remote.Name.Quoted()}.disableautofetch", val); + if (succ) + remote.DisableAutoFetch = !remote.DisableAutoFetch; + } + public void AddSubmodule() { if (CanCreatePopup()) @@ -1483,6 +1516,34 @@ public void UpdateSubmodules() ShowPopup(new UpdateSubmodules(this, null)); } + public async Task AutoUpdateSubmodulesAsync(Models.ICommandLog log) + { + var submodules = await new Commands.QueryUpdatableSubmodules(FullPath, false).GetResultAsync(); + if (submodules.Count == 0) + return; + + do + { + if (_settings.AskBeforeAutoUpdatingSubmodules) + { + var builder = new StringBuilder(); + builder.Append("\n\n"); + foreach (var s in submodules) + builder.Append("- ").Append(s).Append('\n'); + builder.Append("\n"); + + var msg = App.Text("Checkout.WarnUpdatingSubmodules", builder.ToString()); + var shouldContinue = await App.AskConfirmAsync(msg, Models.ConfirmButtonType.YesNo); + if (!shouldContinue) + break; + } + + await new Commands.Submodule(FullPath) + .Use(log) + .UpdateAsync(submodules, false, _settings.EnableRecursiveWhenAutoUpdatingSubmodules, false); + } while (false); + } + public void OpenSubmodule(string submodule) { var selfPage = GetOwnerPage(); @@ -1491,17 +1552,7 @@ public void OpenSubmodule(string submodule) var root = Path.GetFullPath(Path.Combine(FullPath, submodule)); var normalizedPath = root.Replace('\\', '/').TrimEnd('/'); - - var node = Preferences.Instance.FindNode(normalizedPath) ?? - new RepositoryNode - { - Id = normalizedPath, - Name = Path.GetFileName(normalizedPath), - Bookmark = selfPage.Node.Bookmark, - IsRepository = true, - }; - - App.GetLauncher().OpenRepositoryInTab(node, null); + App.GetLauncher().OpenRepositoryInTab(normalizedPath, null); } public void AddWorktree() @@ -1516,21 +1567,16 @@ public async Task PruneWorktreesAsync() await ShowAndStartPopupAsync(new PruneWorktrees(this)); } - public void OpenWorktree(Models.Worktree worktree) + public void OpenWorktree(Worktree worktree) { - var node = Preferences.Instance.FindNode(worktree.FullPath) ?? - new RepositoryNode - { - Id = worktree.FullPath, - Name = Path.GetFileName(worktree.FullPath), - Bookmark = 0, - IsRepository = true, - }; + if (worktree.IsCurrent) + return; - App.GetLauncher().OpenRepositoryInTab(node, null); + var normalizedPath = worktree.FullPath.Replace('\\', '/').TrimEnd('/'); + App.GetLauncher().OpenRepositoryInTab(normalizedPath, null); } - public async Task LockWorktreeAsync(Models.Worktree worktree) + public async Task LockWorktreeAsync(Worktree worktree) { using var lockWatcher = _watcher?.Lock(); var log = CreateLog("Lock Worktree"); @@ -1540,7 +1586,7 @@ public async Task LockWorktreeAsync(Models.Worktree worktree) log.Complete(); } - public async Task UnlockWorktreeAsync(Models.Worktree worktree) + public async Task UnlockWorktreeAsync(Worktree worktree) { using var lockWatcher = _watcher?.Lock(); var log = CreateLog("Unlock Worktree"); @@ -1550,7 +1596,7 @@ public async Task UnlockWorktreeAsync(Models.Worktree worktree) log.Complete(); } - public List GetPreferredOpenAIServices() + public List GetPreferredOpenAIServices() { var services = Preferences.Instance.OpenAIServices; if (services == null || services.Count == 0) @@ -1560,7 +1606,7 @@ public async Task UnlockWorktreeAsync(Models.Worktree worktree) return [services[0]]; var preferred = _settings.PreferredOpenAIService; - var all = new List(); + var all = new List(); foreach (var service in services) { if (service.Name.Equals(preferred, StringComparison.Ordinal)) @@ -1632,16 +1678,19 @@ private LauncherPage GetOwnerPage() return null; } - private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes) + private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes, bool validateExpandedNodes = true) { - var builder = new BranchTreeNode.Builder(_settings.LocalBranchSortMode, _settings.RemoteBranchSortMode); + var builder = new BranchTreeNode.Builder(_uiStates.LocalBranchSortMode, _uiStates.RemoteBranchSortMode); if (string.IsNullOrEmpty(_filter)) { - builder.SetExpandedNodes(_settings.ExpandedBranchNodesInSideBar); + builder.SetExpandedNodes(_uiStates.ExpandedBranchNodesInSideBar); builder.Run(branches, remotes, false); - foreach (var invalid in builder.InvalidExpandedNodes) - _settings.ExpandedBranchNodesInSideBar.Remove(invalid); + if (validateExpandedNodes) + { + foreach (var invalid in builder.InvalidExpandedNodes) + _uiStates.ExpandedBranchNodesInSideBar.Remove(invalid); + } } else { @@ -1655,7 +1704,7 @@ private BranchTreeNode.Builder BuildBranchTree(List branches, Lis builder.Run(visibles, remotes, true); } - var filterMap = _historyFilterCollection.ToMap(); + var filterMap = _uiStates.GetHistoryFiltersMap(); UpdateBranchTreeFilterMode(builder.Locals, filterMap); UpdateBranchTreeFilterMode(builder.Remotes, filterMap); return builder; @@ -1663,7 +1712,7 @@ private BranchTreeNode.Builder BuildBranchTree(List branches, Lis private object BuildVisibleTags() { - switch (_settings.TagSortMode) + switch (_uiStates.TagSortMode) { case Models.TagSortMode.CreatorDate: _tags.Sort((l, r) => r.CreatorDate.CompareTo(l.CreatorDate)); @@ -1687,10 +1736,10 @@ private object BuildVisibleTags() } } - var filterMap = _historyFilterCollection.ToMap(); + var filterMap = _uiStates.GetHistoryFiltersMap(); UpdateTagFilterMode(filterMap); - if (Preferences.Instance.ShowTagsAsTree) + if (_uiStates.ShowTagsAsTree) { var tree = TagCollectionAsTree.Build(visible, _visibleTags as TagCollectionAsTree); foreach (var node in tree.Tree) @@ -1722,7 +1771,7 @@ private object BuildVisibleSubmodules() } } - if (Preferences.Instance.ShowSubmodulesAsTree) + if (_uiStates.ShowSubmodulesAsTree) return SubmoduleCollectionAsTree.Build(visible, _visibleSubmodules as SubmoduleCollectionAsTree); else return new SubmoduleCollectionAsList() { Submodules = visible }; @@ -1730,11 +1779,11 @@ private object BuildVisibleSubmodules() private void RefreshHistoryFilters(bool refresh) { - HistoryFilterMode = _historyFilterCollection.Mode; + HistoryFilterMode = _uiStates.GetHistoryFilterMode(); if (!refresh) return; - var map = _historyFilterCollection.ToMap(); + var map = _uiStates.GetHistoryFiltersMap(); UpdateBranchTreeFilterMode(LocalBranchTrees, map); UpdateBranchTreeFilterMode(RemoteBranchTrees, map); UpdateTagFilterMode(map); @@ -1814,17 +1863,26 @@ private BranchTreeNode FindBranchNode(List nodes, string path) private void AutoFetchByTimer(object sender) { - Dispatcher.UIThread.Invoke(AutoFetchOnUIThread); + try + { + Dispatcher.UIThread.Invoke(AutoFetchOnUIThread); + } + catch + { + // Ignore exception. + } } private async Task AutoFetchOnUIThread() { + if (IsAutoFetching) + return; + + CommandLog log = null; + try { - if (_settings is not { EnableAutoFetch: true }) - return; - - if (!CanCreatePopup()) + if (!Preferences.Instance.EnableAutoFetch || !CanCreatePopup()) { _lastFetchTime = DateTime.Now; return; @@ -1835,52 +1893,49 @@ private async Task AutoFetchOnUIThread() return; var now = DateTime.Now; - var desire = _lastFetchTime.AddMinutes(_settings.AutoFetchInterval); + var desire = _lastFetchTime.AddMinutes(Preferences.Instance.AutoFetchInterval); if (desire > now) return; var remotes = new List(); foreach (var r in _remotes) - remotes.Add(r.Name); - - IsAutoFetching = true; - - if (_settings.FetchAllRemotes) { - foreach (var remote in remotes) - await new Commands.Fetch(FullPath, remote, false, false) { RaiseError = false }.RunAsync(); + if (!r.DisableAutoFetch) + remotes.Add(r.Name); } - else if (remotes.Count > 0) - { - var remote = string.IsNullOrEmpty(_settings.DefaultRemote) ? - remotes.Find(x => x.Equals(_settings.DefaultRemote, StringComparison.Ordinal)) : - remotes[0]; - await new Commands.Fetch(FullPath, remote, false, false) { RaiseError = false }.RunAsync(); - } + if (remotes.Count == 0) + return; + + IsAutoFetching = true; + log = CreateLog("Auto-Fetch"); + + foreach (var remote in remotes) + await new Commands.Fetch(FullPath, remote).Use(log).RunAsync(); _lastFetchTime = DateTime.Now; - IsAutoFetching = false; } catch { // Ignore all exceptions. } + + IsAutoFetching = false; + log?.Complete(); } - private readonly bool _isWorktree = false; private readonly string _gitCommonDir = null; private Models.RepositorySettings _settings = null; - private Models.HistoryFilterCollection _historyFilterCollection = null; + private Models.RepositoryUIStates _uiStates = null; private Models.FilterMode _historyFilterMode = Models.FilterMode.None; private bool _hasAllowedSignersFile = false; + private ulong _queryLocalChangesTimes = 0; private Models.Watcher _watcher = null; private Histories _histories = null; private WorkingCopy _workingCopy = null; private StashesPage _stashesPage = null; private int _selectedViewIndex = 0; - private object _selectedView = null; private int _localBranchesCount = 0; private int _localChangesCount = 0; @@ -1895,7 +1950,7 @@ private async Task AutoFetchOnUIThread() private Models.Branch _currentBranch = null; private List _localBranchTrees = []; private List _remoteBranchTrees = []; - private List _worktrees = []; + private List _worktrees = []; private List _tags = []; private object _visibleTags = null; private List _submodules = []; diff --git a/src/ViewModels/RepositoryCommandPalette.cs b/src/ViewModels/RepositoryCommandPalette.cs index 576bc072b..b607488a6 100644 --- a/src/ViewModels/RepositoryCommandPalette.cs +++ b/src/ViewModels/RepositoryCommandPalette.cs @@ -5,15 +5,29 @@ namespace SourceGit.ViewModels { public class RepositoryCommandPaletteCmd { - public string Key { get; set; } + public string Label { get; set; } + public string Keyword { get; set; } + public string Icon { get; set; } + public bool CloseBeforeExec { get; set; } public Action Action { get; set; } - public string Label => $"{App.Text(Key)}..."; - public RepositoryCommandPaletteCmd(string key, Action action) + public RepositoryCommandPaletteCmd(string labelKey, string keyword, string icon, Action action) { - Key = key; + Label = $"{App.Text(labelKey)}..."; + Keyword = keyword; + Icon = icon; + CloseBeforeExec = true; Action = action; } + + public RepositoryCommandPaletteCmd(string labelKey, string keyword, string icon, ICommandPalette child) + { + Label = $"{App.Text(labelKey)}..."; + Keyword = keyword; + Icon = icon; + CloseBeforeExec = false; + Action = () => child.Open(); + } } public class RepositoryCommandPalette : ICommandPalette @@ -40,61 +54,31 @@ public string Filter } } - public RepositoryCommandPalette(Launcher launcher, Repository repo) + public RepositoryCommandPalette(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); - })); - + // Sub-CommandPalettes + _cmds.Add(new("Blame", "blame", "Blame", new BlameCommandPalette(repo.FullPath))); + _cmds.Add(new("Checkout", "checkout", "Check", new CheckoutCommandPalette(repo))); + _cmds.Add(new("Compare", "compare", "Compare", new CompareCommandPalette(repo, null))); + _cmds.Add(new("FileHistory", "history", "Histories", new FileHistoryCommandPalette(repo.FullPath))); + _cmds.Add(new("Merge", "merge", "Merge", new MergeCommandPalette(repo))); + _cmds.Add(new("OpenFile", "open", "OpenWith", new OpenFileCommandPalette(repo.FullPath))); + _cmds.Add(new("Repository.CustomActions", "custom actions", "Action", new ExecuteCustomActionCommandPalette(repo))); + + // Raw-Actions + _cmds.Add(new("Repository.NewBranch", "create branch", "Branch.Add", () => repo.CreateNewBranch())); + _cmds.Add(new("CreateTag.Title", "create tag", "Tag.Add", () => repo.CreateNewTag())); + _cmds.Add(new("Fetch", "fetch", "Fetch", async () => await repo.FetchAsync(false))); + _cmds.Add(new("Pull.Title", "pull", "Pull", async () => await repo.PullAsync(false))); + _cmds.Add(new("Push", "push", "Push", async () => await repo.PushAsync(false))); + _cmds.Add(new("Stash.Title", "stash", "Stashes.Add", async () => await repo.StashAllAsync(false))); + _cmds.Add(new("Apply.Title", "apply", "ApplyPatch", () => repo.ApplyPatch())); + + _cmds.Sort((l, r) => l.Label.CompareTo(r.Label)); _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; @@ -102,10 +86,16 @@ public void ClearFilter() public void Exec() { + _cmds.Clear(); + _visibleCmds.Clear(); + if (_selectedCmd != null) + { + if (_selectedCmd.CloseBeforeExec) + Close(); + _selectedCmd.Action?.Invoke(); - else - _launcher?.CancelCommandPalette(); + } } private void UpdateVisible() @@ -120,8 +110,8 @@ private void UpdateVisible() foreach (var cmd in _cmds) { - if (cmd.Key.Contains(_filter, StringComparison.OrdinalIgnoreCase) || - cmd.Label.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + if (cmd.Label.Contains(_filter, StringComparison.OrdinalIgnoreCase) || + cmd.Keyword.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visible.Add(cmd); } @@ -134,8 +124,6 @@ private void UpdateVisible() } } - private Launcher _launcher = null; - private Repository _repo = null; private List _cmds = []; private List _visibleCmds = []; private RepositoryCommandPaletteCmd _selectedCmd = null; diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index bc1e59b36..a76194716 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -95,24 +95,16 @@ public bool EnablePruneOnFetch set; } - public bool EnableAutoFetch + public bool AskBeforeAutoUpdatingSubmodules { - get => _repo.Settings.EnableAutoFetch; - set => _repo.Settings.EnableAutoFetch = value; + get => _repo.Settings.AskBeforeAutoUpdatingSubmodules; + set => _repo.Settings.AskBeforeAutoUpdatingSubmodules = value; } - public int? AutoFetchInterval + public bool EnableRecursiveWhenAutoUpdatingSubmodules { - get => _repo.Settings.AutoFetchInterval; - set - { - if (value is null || value < 1) - return; - - var interval = (int)value; - if (_repo.Settings.AutoFetchInterval != interval) - _repo.Settings.AutoFetchInterval = interval; - } + get => _repo.Settings.EnableRecursiveWhenAutoUpdatingSubmodules; + set => _repo.Settings.EnableRecursiveWhenAutoUpdatingSubmodules = value; } public AvaloniaList CommitTemplates @@ -280,6 +272,8 @@ public void MoveSelectedCustomActionDown() public async Task SaveAsync() { + _repo.Settings.Save(); + await SetIfChangedAsync("user.name", UserName, ""); await SetIfChangedAsync("user.email", UserEmail, ""); await SetIfChangedAsync("commit.gpgsign", GPGCommitSigningEnabled ? "true" : "false", "false"); @@ -353,8 +347,8 @@ private async Task ApplyIssueTrackerChangesAsync() } } - private readonly Repository _repo = null; - private readonly Dictionary _cached = null; + private readonly Repository _repo; + private readonly Dictionary _cached; private string _httpProxy; private Models.CommitTemplate _selectedCommitTemplate = null; private Models.IssueTracker _selectedIssueTracker = null; diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index 1f5515b9d..9d7ab9deb 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -8,6 +9,12 @@ namespace SourceGit.ViewModels { + public class RepositoryNodeMinimalInfo + { + public string FriendlyName { get; set; } = string.Empty; + public int Bookmark { get; set; } = 0; + } + public class RepositoryNode : ObservableObject { public string Id @@ -57,6 +64,13 @@ public bool IsInvalid get => _isRepository && !Directory.Exists(_id); } + [JsonIgnore] + public bool IsUnmanaged + { + get; + set; + } = false; + [JsonIgnore] public int Depth { @@ -78,14 +92,15 @@ public List SubNodes public void Open() { - if (IsRepository) + if (!_isRepository) + { + foreach (var subNode in SubNodes) + subNode.Open(); + } + else if (Directory.Exists(_id)) { App.GetLauncher().OpenRepositoryInTab(this, null); - return; } - - foreach (var subNode in SubNodes) - subNode.Open(); } public void Edit() @@ -111,16 +126,14 @@ public void Move() public void OpenInFileManager() { - if (!IsRepository) - return; - Native.OS.OpenInFileManager(_id); + if (_isRepository && Directory.Exists(_id)) + Native.OS.OpenInFileManager(_id); } public void OpenTerminal() { - if (!IsRepository) - return; - Native.OS.OpenTerminal(_id); + if (_isRepository && Directory.Exists(_id)) + Native.OS.OpenTerminal(_id); } public void Delete() @@ -170,6 +183,47 @@ public async Task UpdateStatusAsync(bool force, CancellationToken? token) Status = await new Commands.QueryRepositoryStatus(_id).GetResultAsync(); } + public void LoadMinimalInfo(string gitDir) + { + var savedTo = Path.Combine(gitDir, "sourcegit.node"); + if (!File.Exists(savedTo)) + return; + + try + { + var minimalInfo = JsonSerializer.Deserialize(File.ReadAllText(savedTo), JsonCodeGen.Default.RepositoryNodeMinimalInfo); + if (!string.IsNullOrEmpty(minimalInfo.FriendlyName)) + Name = minimalInfo.FriendlyName; + Bookmark = minimalInfo.Bookmark; + } + catch + { + // Ignore any error and just use default values. + } + } + + public void SaveMinimalInfo(string gitDir) + { + if (!Directory.Exists(gitDir)) + return; + + var savedTo = Path.Combine(gitDir, "sourcegit.node"); + var minimalInfo = new RepositoryNodeMinimalInfo + { + FriendlyName = Name, + Bookmark = Bookmark + }; + + try + { + File.WriteAllText(savedTo, JsonSerializer.Serialize(minimalInfo, JsonCodeGen.Default.RepositoryNodeMinimalInfo)); + } + catch + { + // Ignore any error (e.g. the repository directory was removed while the tab was open). + } + } + private string _id = string.Empty; private string _name = string.Empty; private bool _isRepository = false; diff --git a/src/ViewModels/Reset.cs b/src/ViewModels/Reset.cs index 0e70754b1..11adf1ea7 100644 --- a/src/ViewModels/Reset.cs +++ b/src/ViewModels/Reset.cs @@ -40,6 +40,8 @@ public override async Task Sure() .Use(log) .ExecAsync(); + await _repo.AutoUpdateSubmodulesAsync(log); + log.Complete(); return succ; } diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 4de7b16d6..ef38668cc 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; + using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class RevisionCompare : ObservableObject, IDisposable + public class RevisionCompare : ObservableObject { public bool IsLoading { @@ -26,7 +28,36 @@ public object EndPoint private set => SetProperty(ref _endPoint, value); } - public bool CanSaveAsPatch { get; } + public string LeftSideDesc + { + get => GetDesc(StartPoint); + } + + public string RightSideDesc + { + get => GetDesc(EndPoint); + } + + public bool CanResetToLeft + { + get => !_repo.IsBare && _startPoint != null; + } + + public bool CanResetToRight + { + get => !_repo.IsBare && _endPoint != null; + } + + public bool CanSaveAsPatch + { + get => _startPoint != null && _endPoint != null; + } + + public int TotalChanges + { + get => _totalChanges; + private set => SetProperty(ref _totalChanges, value); + } public List VisibleChanges { @@ -44,7 +75,7 @@ public List SelectedChanges if (value is { Count: 1 }) { var option = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), value[0]); - DiffContext = new DiffContext(_repo, option, _diffContext); + DiffContext = new DiffContext(_repo.FullPath, option, _diffContext); } else { @@ -70,47 +101,45 @@ public DiffContext DiffContext private set => SetProperty(ref _diffContext, value); } - public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint) + public RevisionCompare(Repository repo, Models.Commit startPoint, Models.Commit endPoint) { _repo = repo; _startPoint = (object)startPoint ?? new Models.Null(); _endPoint = (object)endPoint ?? new Models.Null(); - CanSaveAsPatch = startPoint != null && endPoint != null; Refresh(); } - public void Dispose() + public RevisionCompare Clone() { - _repo = null; - _startPoint = null; - _endPoint = null; - _changes?.Clear(); - _visibleChanges?.Clear(); - _selectedChanges?.Clear(); - _searchFilter = null; - _diffContext = null; + return new RevisionCompare(_repo, _startPoint as Models.Commit, _endPoint as Models.Commit); + } + + public void SetTargets(Models.Commit l, Models.Commit r) + { + var hashes = new HashSet(); + hashes.Add(l.SHA); + hashes.Add(r.SHA); + + if (_startPoint is Models.Commit s && + _endPoint is Models.Commit e && + hashes.Contains(s.SHA) && + hashes.Contains(e.SHA)) + return; + + _startPoint = l; + _endPoint = r; + Refresh(); } public void OpenChangeWithExternalDiffTool(Models.Change change) { var opt = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), change); - new Commands.DiffTool(_repo, opt).Open(); + new Commands.DiffTool(_repo.FullPath, opt).Open(); } public void NavigateTo(string commitSHA) { - var launcher = App.GetLauncher(); - if (launcher == null) - return; - - foreach (var page in launcher.Pages) - { - if (page.Data is Repository repo && repo.FullPath.Equals(_repo)) - { - repo.NavigateToCommit(commitSHA); - break; - } - } + _repo?.NavigateToCommit(commitSHA); } public void Swap() @@ -124,14 +153,170 @@ public void Swap() public string GetAbsPath(string path) { - return Native.OS.GetAbsPath(_repo, path); + return Native.OS.GetAbsPath(_repo.FullPath, path); + } + + public async Task ResetToLeftAsync(Models.Change change) + { + var sha = GetSHA(_startPoint); + var log = _repo.CreateLog($"Reset File to '{GetDesc(_startPoint)}'"); + + if (change.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(renamed)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.OriginalPath, sha); + } + else + { + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, sha); + } + + log.Complete(); + } + + public async Task ResetToRightAsync(Models.Change change) + { + var sha = GetSHA(_endPoint); + var log = _repo.CreateLog($"Reset File to '{GetDesc(_endPoint)}'"); + + if (change.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, change.OriginalPath); + if (File.Exists(old)) + await new Commands.Remove(_repo.FullPath, [change.OriginalPath]) + .Use(log) + .ExecAsync(); + + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, sha); + } + else + { + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, sha); + } + + log.Complete(); + } + + public async Task ResetMultipleToLeftAsync(List changes) + { + var sha = GetSHA(_startPoint); + var checkouts = new List(); + var removes = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(old)) + removes.Add(c.Path); + + checkouts.Add(c.OriginalPath); + } + else + { + checkouts.Add(c.Path); + } + } + + var log = _repo.CreateLog($"Reset Files to '{GetDesc(_startPoint)}'"); + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes) + .Use(log) + .ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(checkouts, sha); + + log.Complete(); + } + + public async Task ResetMultipleToRightAsync(List changes) + { + var sha = GetSHA(_endPoint); + var checkouts = new List(); + var removes = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, c.OriginalPath); + if (File.Exists(renamed)) + removes.Add(c.OriginalPath); + + checkouts.Add(c.Path); + } + else + { + checkouts.Add(c.Path); + } + } + + var log = _repo.CreateLog($"Reset Files to '{GetDesc(_endPoint)}'"); + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes) + .Use(log) + .ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(checkouts, sha); + + log.Complete(); } public async Task SaveChangesAsPatchAsync(List changes, string saveTo) { - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); if (succ) - App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); + _repo.SendNotification(App.Text("SaveAsPatchSuccess")); } public void ClearSearchFilter() @@ -165,7 +350,7 @@ private void Refresh() { Task.Run(async () => { - _changes = await new Commands.CompareRevisions(_repo, GetSHA(_startPoint), GetSHA(_endPoint)) + _changes = await new Commands.CompareRevisions(_repo.FullPath, GetSHA(_startPoint), GetSHA(_endPoint)) .ReadAsync() .ConfigureAwait(false); @@ -182,6 +367,7 @@ private void Refresh() Dispatcher.UIThread.Post(() => { + TotalChanges = _changes.Count; VisibleChanges = visible; IsLoading = false; @@ -198,10 +384,16 @@ private string GetSHA(object obj) return obj is Models.Commit commit ? commit.SHA : string.Empty; } - private string _repo; + private string GetDesc(object obj) + { + return obj is Models.Commit commit ? commit.GetFriendlyName() : App.Text("Worktree"); + } + + private Repository _repo; 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/Reword.cs b/src/ViewModels/Reword.cs deleted file mode 100644 index efea03e8d..000000000 --- a/src/ViewModels/Reword.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; - -namespace SourceGit.ViewModels -{ - public class Reword : Popup - { - public Models.Commit Head - { - get; - } - - [Required(ErrorMessage = "Commit message is required!!!")] - public string Message - { - get => _message; - set => SetProperty(ref _message, value, true); - } - - public Reword(Repository repo, Models.Commit head, string oldMessage) - { - _repo = repo; - _oldMessage = oldMessage; - _message = _oldMessage; - Head = head; - } - - public override async Task Sure() - { - if (string.Compare(_message, _oldMessage, StringComparison.Ordinal) == 0) - return true; - - using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = "Editing HEAD message ..."; - - var log = _repo.CreateLog("Reword HEAD"); - 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; - - foreach (var c in changes) - { - if (c.Index != Models.ChangeState.None) - { - needAutoStash = true; - break; - } - } - - if (needAutoStash) - { - succ = await new Commands.Stash(_repo.FullPath) - .Use(log) - .PushAsync("REWORD_AUTO_STASH"); - if (!succ) - { - log.Complete(); - return false; - } - } - - succ = await new Commands.Commit(_repo.FullPath, _message, signOff, noVerify, true, false) - .Use(log) - .RunAsync(); - - if (succ && needAutoStash) - await new Commands.Stash(_repo.FullPath) - .Use(log) - .PopAsync("stash@{0}"); - - log.Complete(); - return succ; - } - - private readonly Repository _repo; - private readonly string _oldMessage; - private string _message; - } -} diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index e7316dba5..f9ac3e41b 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading.Tasks; @@ -56,7 +57,7 @@ public override async Task Sure() { if (string.IsNullOrEmpty(_customDir)) { - App.RaiseException(null, "Missing root directory to scan!"); + Models.Notification.Send(null, "Missing root directory to scan!", true); return false; } @@ -66,7 +67,7 @@ public override async Task Sure() { if (_selected == null || string.IsNullOrEmpty(_selected.Path)) { - App.RaiseException(null, "Missing root directory to scan!"); + Models.Notification.Send(null, "Missing root directory to scan!", true); return false; } @@ -92,15 +93,17 @@ public override async Task Sure() await minDelay; var normalizedRoot = rootDir.FullName.Replace('\\', '/').TrimEnd('/'); - foreach (var f in found) + for (var i = 0; i < found.Count; i++) { + var f = found[i]; + ProgressDescription = $"Registering ({i + 1}/{found.Count}) {f}..."; var parent = new DirectoryInfo(f).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); - if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) + if (parent.Equals(normalizedRoot, StringComparison.OrdinalIgnoreCase)) { var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(f, null, false, false); await node.UpdateStatusAsync(false, null); } - else if (parent.StartsWith(normalizedRoot, StringComparison.Ordinal)) + else if (parent.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) { var relative = parent.Substring(normalizedRoot.Length).TrimStart('/'); var group = FindOrCreateGroupRecursive(Preferences.Instance.RepositoryNodes, relative); @@ -120,7 +123,7 @@ private void GetManagedRepositories(List group, HashSet foreach (var node in group) { if (node.IsRepository) - repos.Add(node.Id); + repos.Add(OperatingSystem.IsLinux() ? node.Id : node.Id.ToLower(CultureInfo.CurrentCulture)); else GetManagedRepositories(node.SubNodes, repos); } @@ -138,7 +141,7 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List ProgressDescription = $"Scanning {subdir.FullName}..."; var normalizedSelf = subdir.FullName.Replace('\\', '/').TrimEnd('/'); - if (_managed.Contains(normalizedSelf)) + if (IsManaged(normalizedSelf)) continue; var gitDir = Path.Combine(subdir.FullName, ".git"); @@ -148,7 +151,7 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { var normalized = test.StdOut.Trim().Replace('\\', '/').TrimEnd('/'); - if (!_managed.Contains(normalized)) + if (!IsManaged(normalized)) outs.Add(normalized); } @@ -200,6 +203,14 @@ private RepositoryNode FindOrCreateGroup(List collection, string return added; } + private bool IsManaged(string path) + { + if (OperatingSystem.IsLinux()) + return _managed.Contains(path); + + return _managed.Contains(path.ToLower(CultureInfo.CurrentCulture)); + } + private HashSet _managed = new(); private bool _useCustomDir = false; private string _customDir = string.Empty; diff --git a/src/ViewModels/SearchCommitContext.cs b/src/ViewModels/SearchCommitContext.cs index 374336e5e..e67046a9c 100644 --- a/src/ViewModels/SearchCommitContext.cs +++ b/src/ViewModels/SearchCommitContext.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class SearchCommitContext : ObservableObject, IDisposable + public class SearchCommitContext : ObservableObject { public int Method { @@ -41,7 +42,7 @@ public bool OnlySearchCurrentBranch } } - public List Suggestions + public List Suggestions { get => _suggestions; private set => SetProperty(ref _suggestions, value); @@ -74,14 +75,6 @@ public SearchCommitContext(Repository repo) _repo = repo; } - public void Dispose() - { - _repo = null; - _suggestions?.Clear(); - _results?.Clear(); - _worktreeFiles?.Clear(); - } - public void ClearFilter() { Filter = string.Empty; @@ -105,6 +98,12 @@ public void StartSearch() IsQuerying = true; + if (_cancellation is { IsCancellationRequested: false }) + _cancellation.Cancel(); + + _cancellation = new(); + var token = _cancellation.Token; + Task.Run(async () => { var result = new List(); @@ -158,17 +157,29 @@ public void StartSearch() Dispatcher.UIThread.Post(() => { - IsQuerying = false; + if (token.IsCancellationRequested) + return; + IsQuerying = false; if (_repo.IsSearchingCommits) + { Results = result; + if (method == Models.CommitSearchMethod.BySHA && result.Count == 1) + Selected = result[0]; + } }); - }); + }, token); } public void EndSearch() { + if (_cancellation is { IsCancellationRequested: false }) + _cancellation.Cancel(); + _worktreeFiles = null; + _authors = null; + + IsQuerying = false; Suggestions = null; Results = null; GC.Collect(); @@ -176,66 +187,116 @@ public void EndSearch() private void UpdateSuggestions() { - if (_method != (int)Models.CommitSearchMethod.ByPath || _requestingWorktreeFiles) + if (_method == (int)Models.CommitSearchMethod.ByAuthor) { - Suggestions = null; - return; - } + if (_authors == null) + { + if (_requestingAuthors) + return; - if (_worktreeFiles == null) - { - _requestingWorktreeFiles = true; + _requestingAuthors = true; + + Task.Run(async () => + { + var authors = await new Commands.QueryAuthors(_repo.FullPath) + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + _requestingAuthors = false; + + if (_repo.IsSearchingCommits) + { + _authors = authors; + UpdateSuggestions(); + } + }); + }); + + return; + } - Task.Run(async () => + if (_authors.Count == 0 || _filter.Length < 2) { - var files = await new Commands.QueryRevisionFileNames(_repo.FullPath, "HEAD") - .GetResultAsync() - .ConfigureAwait(false); + Suggestions = null; + return; + } - Dispatcher.UIThread.Post(() => + var matched = new List(); + foreach (var author in _authors) + { + if (author.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) || + author.Email.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + matched.Add(author); + } + + Suggestions = matched; + } + else if (_method == (int)Models.CommitSearchMethod.ByPath) + { + if (_worktreeFiles == null) + { + if (_requestingWorktreeFiles) + return; + + _requestingWorktreeFiles = true; + + Task.Run(async () => { - _requestingWorktreeFiles = false; + var files = await new Commands.QueryRevisionFileNames(_repo.FullPath, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); - if (_repo.IsSearchingCommits) + Dispatcher.UIThread.Post(() => { - _worktreeFiles = files; - UpdateSuggestions(); - } + _requestingWorktreeFiles = false; + + if (_repo.IsSearchingCommits) + { + _worktreeFiles = files; + UpdateSuggestions(); + } + }); }); - }); - return; - } + return; + } - if (_worktreeFiles.Count == 0 || _filter.Length < 3) - { - Suggestions = null; - 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) + var matched = new List(); + foreach (var file in _worktreeFiles) { - matched.Add(file); - if (matched.Count > 100) - break; + if (file.Contains(_filter, StringComparison.OrdinalIgnoreCase) && file.Length != _filter.Length) + matched.Add(file); } - } - Suggestions = matched; + Suggestions = matched; + } + else + { + Suggestions = null; + return; + } } private Repository _repo = null; + private CancellationTokenSource _cancellation = 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; + private bool _requestingAuthors = false; + private List _authors = null; + private List _suggestions = null; } } diff --git a/src/ViewModels/SquashOrFixupHead.cs b/src/ViewModels/SquashOrFixupHead.cs deleted file mode 100644 index 18b5b6f91..000000000 --- a/src/ViewModels/SquashOrFixupHead.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; - -namespace SourceGit.ViewModels -{ - public class SquashOrFixupHead : Popup - { - public bool IsFixupMode - { - get; - } - - public Models.Commit Target - { - get; - } - - [Required(ErrorMessage = "Commit message is required!!!")] - public string Message - { - get => _message; - set => SetProperty(ref _message, value, true); - } - - public SquashOrFixupHead(Repository repo, Models.Commit target, string message, bool fixup) - { - IsFixupMode = fixup; - Target = target; - - _repo = repo; - _message = message; - } - - public override async Task Sure() - { - using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = IsFixupMode ? "Fixup ..." : "Squashing ..."; - - 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; - - foreach (var c in changes) - { - if (c.Index != Models.ChangeState.None) - { - needAutoStash = true; - break; - } - } - - if (needAutoStash) - { - succ = await new Commands.Stash(_repo.FullPath) - .Use(log) - .PushAsync(IsFixupMode ? "FIXUP_AUTO_STASH" : "SQUASH_AUTO_STASH"); - if (!succ) - { - log.Complete(); - return false; - } - } - - succ = await new Commands.Reset(_repo.FullPath, Target.SHA, "--soft") - .Use(log) - .ExecAsync(); - - if (succ) - succ = await new Commands.Commit(_repo.FullPath, _message, signOff, noVerify, true, false) - .Use(log) - .RunAsync(); - - if (succ && needAutoStash) - await new Commands.Stash(_repo.FullPath) - .Use(log) - .PopAsync("stash@{0}"); - - log.Complete(); - return succ; - } - - private readonly Repository _repo; - private string _message; - } -} diff --git a/src/ViewModels/StashChanges.cs b/src/ViewModels/StashChanges.cs index 154c89b2c..bde579b8a 100644 --- a/src/ViewModels/StashChanges.cs +++ b/src/ViewModels/StashChanges.cs @@ -20,18 +20,18 @@ public bool HasSelectedFiles public bool IncludeUntracked { - get => _repo.Settings.IncludeUntrackedWhenStash; - set => _repo.Settings.IncludeUntrackedWhenStash = value; + get => _repo.UIStates.IncludeUntrackedWhenStash; + set => _repo.UIStates.IncludeUntrackedWhenStash = value; } public bool OnlyStaged { - get => _repo.Settings.OnlyStagedWhenStash; + get => _repo.UIStates.OnlyStagedWhenStash; set { - if (_repo.Settings.OnlyStagedWhenStash != value) + if (_repo.UIStates.OnlyStagedWhenStash != value) { - _repo.Settings.OnlyStagedWhenStash = value; + _repo.UIStates.OnlyStagedWhenStash = value; OnPropertyChanged(); } } @@ -39,8 +39,8 @@ public bool OnlyStaged public int ChangesAfterStashing { - get => _repo.Settings.ChangesAfterStashing; - set => _repo.Settings.ChangesAfterStashing = value; + get => _repo.UIStates.ChangesAfterStashing; + set => _repo.UIStates.ChangesAfterStashing = value; } public StashChanges(Repository repo, List selectedChanges) diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index 96f01c7a0..9f4874dba 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Avalonia.Threading; @@ -7,7 +8,7 @@ namespace SourceGit.ViewModels { - public class StashesPage : ObservableObject, IDisposable + public class StashesPage : ObservableObject { public List Stashes { @@ -62,7 +63,7 @@ public Models.Stash SelectedStash var untracked = new List(); if (value.Parents.Count == 3) { - untracked = await new Commands.CompareRevisions(_repo.FullPath, Models.Commit.EmptyTreeSHA1, value.Parents[2]) + untracked = await new Commands.CompareRevisions(_repo.FullPath, value.UntrackedParent, value.Parents[2]) .ReadAsync() .ConfigureAwait(false); @@ -106,7 +107,7 @@ public List SelectedChanges if (value is not { Count: 1 }) DiffContext = null; else if (_untracked.Contains(value[0])) - DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], value[0]), _diffContext); + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], value[0]), _diffContext); else DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, value[0]), _diffContext); } @@ -124,18 +125,6 @@ public StashesPage(Repository repo) _repo = repo; } - public void Dispose() - { - _stashes?.Clear(); - _changes?.Clear(); - _selectedChanges?.Clear(); - _untracked.Clear(); - - _repo = null; - _selectedStash = null; - _diffContext = null; - } - public void ClearSearchFilter() { SearchFilter = string.Empty; @@ -152,6 +141,12 @@ public void Apply(Models.Stash stash) _repo.ShowPopup(new ApplyStash(_repo, stash)); } + public void CheckoutBranch(Models.Stash stash) + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new CheckoutBranchFromStash(_repo, stash)); + } + public void Drop(Models.Stash stash) { if (_repo.CanCreatePopup()) @@ -166,52 +161,37 @@ public async Task SaveStashAsPatchAsync(Models.Stash stash, string saveTo) .ConfigureAwait(false); foreach (var c in changes) - opts.Add(new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, c)); + opts.Add(new Models.DiffOption(stash.Parents[0], stash.SHA, c)); if (stash.Parents.Count == 3) { - var untracked = await new Commands.CompareRevisions(_repo.FullPath, Models.Commit.EmptyTreeSHA1, stash.Parents[2]) + var untracked = await new Commands.CompareRevisions(_repo.FullPath, stash.UntrackedParent, stash.Parents[2]) .ReadAsync() .ConfigureAwait(false); foreach (var c in untracked) - opts.Add(new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], c)); + opts.Add(new Models.DiffOption(stash.UntrackedParent, stash.Parents[2], c)); changes.AddRange(untracked); } var succ = await Commands.SaveChangesAsPatch.ProcessStashChangesAsync(_repo.FullPath, opts, saveTo); if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); + _repo.SendNotification(App.Text("SaveAsPatchSuccess")); } public void OpenChangeWithExternalDiffTool(Models.Change change) { Models.DiffOption opt; if (_untracked.Contains(change)) - opt = new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], change); + opt = new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], change); else opt = new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, change); new Commands.DiffTool(_repo.FullPath, opt).Open(); } - public async Task CheckoutSingleFileAsync(Models.Change change) - { - 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 async Task CheckoutMultipleFileAsync(List changes) + public async Task CheckoutFilesAsync(List changes) { var untracked = new List(); var added = new List(); @@ -247,6 +227,34 @@ public async Task CheckoutMultipleFileAsync(List changes) log.Complete(); } + public async Task ApplySelectedChanges(List changes) + { + if (_selectedStash == null) + return; + + var opts = new List(); + foreach (var c in changes) + { + if (_untracked.Contains(c) && _selectedStash.Parents.Count == 3) + opts.Add(new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], c)); + else + opts.Add(new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, c)); + } + + var saveTo = Path.GetTempFileName(); + var succ = await Commands.SaveChangesAsPatch.ProcessStashChangesAsync(_repo.FullPath, opts, saveTo); + if (!succ) + return; + + var log = _repo.CreateLog($"Apply changes from '{_selectedStash.Name}'"); + await new Commands.Apply(_repo.FullPath, saveTo, true, string.Empty, string.Empty) + .Use(log) + .ExecAsync(); + + log.Complete(); + File.Delete(saveTo); + } + private void RefreshVisible() { if (string.IsNullOrEmpty(_searchFilter)) diff --git a/src/ViewModels/Statistics.cs b/src/ViewModels/Statistics.cs index a16c94ecc..add2423e2 100644 --- a/src/ViewModels/Statistics.cs +++ b/src/ViewModels/Statistics.cs @@ -1,8 +1,6 @@ -using System.Threading.Tasks; - -using Avalonia.Media; +using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -15,50 +13,82 @@ public bool IsLoading private set => SetProperty(ref _isLoading, value); } - public int SelectedIndex + public List Branches { - get => _selectedIndex; - set - { - if (SetProperty(ref _selectedIndex, value)) - RefreshReport(); - } + get => _branches; + private set => SetProperty(ref _branches, value); } - public Models.StatisticsReport SelectedReport + public Models.Branch SelectedBranch { - get => _selectedReport; - private set + get => _selectedBranch; + set { - value?.ChangeAuthor(null); - SetProperty(ref _selectedReport, value); + if (SetProperty(ref _selectedBranch, value)) + LoadStatistics(); } } - public uint SampleColor + public Models.StatisticsMode ViewMode { - get => Preferences.Instance.StatisticsSampleColor; + get => _viewMode; set { - if (value != Preferences.Instance.StatisticsSampleColor) - { - Preferences.Instance.StatisticsSampleColor = value; - OnPropertyChanged(nameof(SampleBrush)); - _selectedReport?.ChangeColor(value); - } + if (SetProperty(ref _viewMode, value)) + RefreshReport(); } } - public IBrush SampleBrush + public Models.StatisticsReport SelectedReport + { + get => _selectedReport; + private set => SetProperty(ref _selectedReport, value); + } + + public Models.StatisticsSamples Samples { - get => new SolidColorBrush(SampleColor); + get => _samples; + private set => SetProperty(ref _samples, value); } public Statistics(string repo) { + _repo = repo; + LoadBranches(); + LoadStatistics(); + } + + public void ChangeAuthor(Models.StatisticsAuthor author) + { + if (SelectedReport == null) + return; + + Samples = SelectedReport.GetSamples(author); + } + + private void LoadBranches() + { + Task.Run(async () => + { + var branches = await new Commands.QueryBranches(_repo) + .GetResultAsync() + .ConfigureAwait(false); + + branches.Insert(0, _selectedBranch); + Dispatcher.UIThread.Post(() => Branches = branches); + }); + } + + private void LoadStatistics() + { + IsLoading = true; + Task.Run(async () => { - var result = await new Commands.Statistics(repo, Preferences.Instance.MaxHistoryCommits).ReadAsync().ConfigureAwait(false); + var result = await new Commands.Statistics(_repo, Preferences.Instance.MaxHistoryCommits, _selectedBranch) + .ReadAsync() + .ConfigureAwait(false); + Dispatcher.UIThread.Post(() => { _data = result; @@ -73,20 +103,23 @@ private void RefreshReport() if (_data == null) return; - var report = _selectedIndex switch + SelectedReport = _viewMode switch { - 0 => _data.All, - 1 => _data.Month, + Models.StatisticsMode.All => _data.All, + Models.StatisticsMode.ThisMonth => _data.Month, _ => _data.Week, }; - report.ChangeColor(SampleColor); - SelectedReport = report; + Samples = SelectedReport.GetSamples(null); } + private string _repo = null; private bool _isLoading = true; + private List _branches = new List(); + private Models.Branch _selectedBranch = new Models.Branch() { Name = "--- (All)", IsLocal = true, FullName = "", Head = "---" }; // Fake branch to represent all branches private Models.Statistics _data = null; + private Models.StatisticsMode _viewMode = Models.StatisticsMode.All; private Models.StatisticsReport _selectedReport = null; - private int _selectedIndex = 0; + private Models.StatisticsSamples _samples = null; } } diff --git a/src/ViewModels/BranchCompare.cs b/src/ViewModels/SubmoduleRevisionCompare.cs similarity index 60% rename from src/ViewModels/BranchCompare.cs rename to src/ViewModels/SubmoduleRevisionCompare.cs index e7f17e490..bbf41a2db 100644 --- a/src/ViewModels/BranchCompare.cs +++ b/src/ViewModels/SubmoduleRevisionCompare.cs @@ -6,41 +6,30 @@ namespace SourceGit.ViewModels { - public class BranchCompare : ObservableObject + public class SubmoduleRevisionCompare : ObservableObject { - public string RepositoryPath - { - get => _repo; - } - public bool IsLoading { get => _isLoading; private set => SetProperty(ref _isLoading, value); } - public Models.Branch Base + public Models.Commit Base { - get => _based; - private set => SetProperty(ref _based, value); + get => _base; + private set => SetProperty(ref _base, value); } - public Models.Branch To + public Models.Commit To { get => _to; private set => SetProperty(ref _to, value); } - public Models.Commit BaseHead - { - get => _baseHead; - private set => SetProperty(ref _baseHead, value); - } - - public Models.Commit ToHead + public int TotalChanges { - get => _toHead; - private set => SetProperty(ref _toHead, value); + get => _totalChanges; + private set => SetProperty(ref _totalChanges, value); } public List VisibleChanges @@ -57,7 +46,7 @@ public List SelectedChanges if (SetProperty(ref _selectedChanges, value)) { if (value is { Count: 1 }) - DiffContext = new DiffContext(_repo, new Models.DiffOption(_based.Head, _to.Head, value[0]), _diffContext); + DiffContext = new DiffContext(_repo, new Models.DiffOption(_base.SHA, _to.SHA, value[0]), _diffContext); else DiffContext = null; } @@ -80,41 +69,18 @@ public DiffContext DiffContext private set => SetProperty(ref _diffContext, value); } - public BranchCompare(string repo, Models.Branch baseBranch, Models.Branch toBranch) + public SubmoduleRevisionCompare(Models.SubmoduleDiff diff) { - _repo = repo; - _based = baseBranch; - _to = toBranch; + _repo = diff.FullPath; + _base = diff.Old.Commit; + _to = diff.New.Commit; Refresh(); } - public void NavigateTo(string commitSHA) - { - var launcher = App.GetLauncher(); - if (launcher == null) - return; - - foreach (var page in launcher.Pages) - { - if (page.Data is Repository repo && repo.FullPath.Equals(_repo)) - { - repo.NavigateToCommit(commitSHA); - break; - } - } - } - public void Swap() { - (Base, To) = (_to, _based); - - VisibleChanges = []; - SelectedChanges = []; - - if (_baseHead != null) - (BaseHead, ToHead) = (_toHead, _baseHead); - + (Base, To) = (To, Base); Refresh(); } @@ -128,37 +94,25 @@ public string GetAbsPath(string path) return Native.OS.GetAbsPath(_repo, path); } - public async Task SaveChangesAsPatchAsync(List changes, string saveTo) + public void OpenInExternalDiffTool(Models.Change change) { - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _based.Head, _to.Head, saveTo); - if (succ) - App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); + new Commands.DiffTool(_repo, new Models.DiffOption(_base.SHA, _to.SHA, change)).Open(); + } + + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) + { + return await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _base.SHA, _to.SHA, saveTo); } private void Refresh() { IsLoading = true; + VisibleChanges = []; + SelectedChanges = []; Task.Run(async () => { - if (_baseHead == null) - { - var baseHead = await new Commands.QuerySingleCommit(_repo, _based.Head) - .GetResultAsync() - .ConfigureAwait(false); - - var toHead = await new Commands.QuerySingleCommit(_repo, _to.Head) - .GetResultAsync() - .ConfigureAwait(false); - - Dispatcher.UIThread.Post(() => - { - BaseHead = baseHead; - ToHead = toHead; - }); - } - - _changes = await new Commands.CompareRevisions(_repo, _based.Head, _to.Head) + _changes = await new Commands.CompareRevisions(_repo, _base.SHA, _to.SHA) .ReadAsync() .ConfigureAwait(false); @@ -175,6 +129,7 @@ private void Refresh() Dispatcher.UIThread.Post(() => { + TotalChanges = _changes.Count; VisibleChanges = visible; IsLoading = false; @@ -210,10 +165,9 @@ private void RefreshVisible() private string _repo; private bool _isLoading = true; - private Models.Branch _based = null; - private Models.Branch _to = null; - private Models.Commit _baseHead = null; - private Models.Commit _toHead = null; + private Models.Commit _base = null; + private Models.Commit _to = null; + private int _totalChanges = 0; private List _changes = null; private List _visibleChanges = null; private List _selectedChanges = null; diff --git a/src/ViewModels/TagCollection.cs b/src/ViewModels/TagCollection.cs index 50503dc22..05b04f5b1 100644 --- a/src/ViewModels/TagCollection.cs +++ b/src/ViewModels/TagCollection.cs @@ -13,7 +13,7 @@ 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 ulong CreatorDate { get; private set; } public string Message { get; private set; } public TagToolTip(Models.Tag t) @@ -21,7 +21,7 @@ public TagToolTip(Models.Tag t) Name = t.Name; IsAnnotated = t.IsAnnotated; Creator = t.Creator; - CreatorDateStr = t.CreatorDateStr; + CreatorDate = t.CreatorDate; Message = t.Message; } } diff --git a/src/ViewModels/TextDiffContext.cs b/src/ViewModels/TextDiffContext.cs index 90c7df6e2..fd1be431b 100644 --- a/src/ViewModels/TextDiffContext.cs +++ b/src/ViewModels/TextDiffContext.cs @@ -5,8 +5,6 @@ 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) @@ -43,7 +41,7 @@ public BlockNavigation BlockNavigation set => SetProperty(ref _blockNavigation, value); } - public TextDiffDisplayRange DisplayRange + public TextLineRange DisplayRange { get => _displayRange; set => SetProperty(ref _displayRange, value); @@ -161,7 +159,7 @@ protected void TryKeepPrevState(TextDiffContext prev, List protected Vector _scrollOffset = Vector.Zero; protected BlockNavigation _blockNavigation = null; - private TextDiffDisplayRange _displayRange = null; + private TextLineRange _displayRange = null; private TextDiffSelectedChunk _selectedChunk = null; } @@ -223,7 +221,7 @@ public override TextDiffContext SwitchMode() return new CombinedTextDiff(_option, _data, this); } - public void ConvertsToCombinedRange(ref int startLine, ref int endLine, bool isOldSide) + public void GetCombinedRangeForSingleSide(ref int startLine, ref int endLine, bool isOldSide) { endLine = Math.Min(endLine, _data.Lines.Count - 1); @@ -262,6 +260,32 @@ public void ConvertsToCombinedRange(ref int startLine, ref int endLine, bool isO endLine = _data.Lines.IndexOf(endContent); } + public void GetCombinedRangeForBothSides(ref int startLine, ref int endLine, bool isOldSide) + { + var fromSide = isOldSide ? Old : New; + endLine = Math.Min(endLine, fromSide.Count - 1); + + // Since this function is only used for auto-detected hunk, we just need to find out the a first changed line + // and then use `FindRangeByIndex` to get the range of hunk. + for (int i = startLine; i <= endLine; i++) + { + var line = fromSide[i]; + if (line.Type == Models.TextDiffLineType.Added || line.Type == Models.TextDiffLineType.Deleted) + { + (startLine, endLine) = FindRangeByIndex(_data.Lines, _data.Lines.IndexOf(line)); + return; + } + + if (line.Type == Models.TextDiffLineType.None) + { + var otherSide = isOldSide ? New : Old; + var changedLine = otherSide[i]; // Find the changed line on the other side in the same position + (startLine, endLine) = FindRangeByIndex(_data.Lines, _data.Lines.IndexOf(changedLine)); + return; + } + } + } + private void FillEmptyLines() { if (Old.Count < New.Count) diff --git a/src/ViewModels/TextLineRange.cs b/src/ViewModels/TextLineRange.cs new file mode 100644 index 000000000..63a9fe99e --- /dev/null +++ b/src/ViewModels/TextLineRange.cs @@ -0,0 +1,4 @@ +namespace SourceGit.ViewModels +{ + public record TextLineRange(int Start, int End); +} diff --git a/src/ViewModels/UpdateSubmodules.cs b/src/ViewModels/UpdateSubmodules.cs index e66a85dd1..323a60b2d 100644 --- a/src/ViewModels/UpdateSubmodules.cs +++ b/src/ViewModels/UpdateSubmodules.cs @@ -69,6 +69,8 @@ public UpdateSubmodules(Repository repo, Models.Submodule selected) IsEnableInitVisible = true; HasPreSelectedSubmodule = false; } + + EnableRecursive = _repo.Settings.EnableRecursiveWhenAutoUpdatingSubmodules; } public override async Task Sure() @@ -96,6 +98,7 @@ public override async Task Sure() .UpdateAsync(targets, EnableInit, EnableRecursive, EnableRemote); log.Complete(); + _repo.MarkSubmodulesDirtyManually(); return true; } diff --git a/src/ViewModels/ViewLogs.cs b/src/ViewModels/ViewLogs.cs index 8dc3e9605..21ab81ab7 100644 --- a/src/ViewModels/ViewLogs.cs +++ b/src/ViewModels/ViewLogs.cs @@ -19,7 +19,6 @@ 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 1fa1797a8..49539292d 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -104,7 +104,7 @@ public async Task GetRepositoryRootAsync(string path) { if (!Preferences.Instance.IsGitConfigured()) { - App.RaiseException(string.Empty, App.Text("NotConfigured")); + Models.Notification.Send(null, App.Text("NotConfigured"), true); return null; } @@ -128,19 +128,6 @@ public async Task GetRepositoryRootAsync(string path) return rs.StdOut.Trim(); } - public void InitRepository(string path, RepositoryNode parent, string reason) - { - if (!Preferences.Instance.IsGitConfigured()) - { - App.RaiseException(string.Empty, App.Text("NotConfigured")); - return; - } - - var activePage = App.GetLauncher().ActivePage; - if (activePage != null && activePage.CanCreatePopup()) - 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); @@ -154,7 +141,7 @@ public void Clone() { if (!Preferences.Instance.IsGitConfigured()) { - App.RaiseException(string.Empty, App.Text("NotConfigured")); + Models.Notification.Send(null, App.Text("NotConfigured"), true); return; } @@ -163,10 +150,23 @@ public void Clone() activePage.Popup = new Clone(activePage.Node.Id); } + public void OpenLocalRepository() + { + if (!Preferences.Instance.IsGitConfigured()) + { + Models.Notification.Send(null, App.Text("NotConfigured"), true); + return; + } + + var activePage = App.GetLauncher().ActivePage; + if (activePage != null && activePage.CanCreatePopup()) + activePage.Popup = new OpenLocalRepository(activePage.Node.Id, null); + } + public void OpenTerminal() { if (!Preferences.Instance.IsGitConfigured()) - App.RaiseException(string.Empty, App.Text("NotConfigured")); + Models.Notification.Send(null, App.Text("NotConfigured"), true); else Native.OS.OpenTerminal(null); } @@ -175,7 +175,7 @@ public void ScanDefaultCloneDir() { if (!Preferences.Instance.IsGitConfigured()) { - App.RaiseException(string.Empty, App.Text("NotConfigured")); + Models.Notification.Send(null, App.Text("NotConfigured"), true); return; } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 68b5fd11b..c0f273529 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; using System.Threading.Tasks; - -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class WorkingCopy : ObservableObject, IDisposable + public class WorkingCopy : ObservableObject { public Repository Repository { @@ -41,6 +38,12 @@ public bool HasUnsolvedConflicts set => SetProperty(ref _hasUnsolvedConflicts, value); } + public bool CanSwitchBranchDirectly + { + get; + set; + } = true; + public InProgressContext InProgressContext { get => _inProgressContext; @@ -67,14 +70,14 @@ public bool IsCommitting public bool EnableSignOff { - get => _repo.Settings.EnableSignOffForCommit; - set => _repo.Settings.EnableSignOffForCommit = value; + get => _repo.UIStates.EnableSignOffForCommit; + set => _repo.UIStates.EnableSignOffForCommit = value; } public bool NoVerifyOnCommit { - get => _repo.Settings.NoVerifyOnCommit; - set => _repo.Settings.NoVerifyOnCommit = value; + get => _repo.UIStates.NoVerifyOnCommit; + set => _repo.UIStates.NoVerifyOnCommit = value; } public bool UseAmend @@ -89,7 +92,7 @@ public bool UseAmend var currentBranch = _repo.CurrentBranch; if (currentBranch == null) { - App.RaiseException(_repo.FullPath, "No commits to amend!!!"); + _repo.SendNotification("No commits to amend!!!", true); _useAmend = false; OnPropertyChanged(); return; @@ -103,7 +106,7 @@ public bool UseAmend ResetAuthor = false; } - Staged = GetStagedChanges(); + Staged = GetStagedChanges(_cached); VisibleStaged = GetVisibleChanges(_staged); SelectedStaged = []; } @@ -226,113 +229,89 @@ public WorkingCopy(Repository repo) _repo = repo; } - public void Dispose() - { - _repo = null; - _inProgressContext = null; - - _selectedUnstaged.Clear(); - OnPropertyChanged(nameof(SelectedUnstaged)); - - _selectedStaged.Clear(); - OnPropertyChanged(nameof(SelectedStaged)); - - _visibleUnstaged.Clear(); - OnPropertyChanged(nameof(VisibleUnstaged)); - - _visibleStaged.Clear(); - OnPropertyChanged(nameof(VisibleStaged)); - - _unstaged.Clear(); - OnPropertyChanged(nameof(Unstaged)); - - _staged.Clear(); - OnPropertyChanged(nameof(Staged)); - - _detailContext = null; - _commitMessage = string.Empty; - } - - public void SetData(List changes, CancellationToken cancellationToken) + public void SetData(List changes) { if (!IsChanged(_cached, changes)) { - // Just force refresh selected changes. - Dispatcher.UIThread.Invoke(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - HasUnsolvedConflicts = _cached.Find(x => x.IsConflicted) != null; - UpdateDetail(); - UpdateInProgressState(); - }); - + HasUnsolvedConflicts = _cached.Find(x => x.IsConflicted) != null; + UpdateInProgressState(); + UpdateDetail(); return; } - _cached = changes; - var lastSelectedUnstaged = new HashSet(); - var lastSelectedStaged = new HashSet(); if (_selectedUnstaged is { Count: > 0 }) { foreach (var c in _selectedUnstaged) lastSelectedUnstaged.Add(c.Path); } - else if (_selectedStaged is { Count: > 0 }) - { - foreach (var c in _selectedStaged) - lastSelectedStaged.Add(c.Path); - } var unstaged = new List(); + var visibleUnstaged = new List(); + var selectedUnstaged = new List(); + var noFilter = string.IsNullOrEmpty(_filter); var hasConflict = false; + var canSwitchDirectly = true; foreach (var c in changes) { if (c.WorkTree != Models.ChangeState.None) { unstaged.Add(c); hasConflict |= c.IsConflicted; + + if (noFilter || c.Path.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + { + visibleUnstaged.Add(c); + if (lastSelectedUnstaged.Contains(c.Path)) + selectedUnstaged.Add(c); + } } - } - var visibleUnstaged = GetVisibleChanges(unstaged); - var selectedUnstaged = new List(); - foreach (var c in visibleUnstaged) - { - if (lastSelectedUnstaged.Contains(c.Path)) - selectedUnstaged.Add(c); - } + if (!canSwitchDirectly) + continue; + + if (c.WorkTree == Models.ChangeState.Untracked || c.Index == Models.ChangeState.Added) + continue; - var staged = GetStagedChanges(); + canSwitchDirectly = false; + } + var staged = GetStagedChanges(changes); var visibleStaged = GetVisibleChanges(staged); var selectedStaged = new List(); - foreach (var c in visibleStaged) + if (_selectedStaged is { Count: > 0 }) { - if (lastSelectedStaged.Contains(c.Path)) - selectedStaged.Add(c); + var set = new HashSet(); + foreach (var c in _selectedStaged) + set.Add(c.Path); + + foreach (var c in visibleStaged) + { + if (set.Contains(c.Path)) + selectedStaged.Add(c); + } } - Dispatcher.UIThread.Invoke(() => + if (selectedUnstaged.Count == 0 && selectedStaged.Count == 0 && hasConflict) { - if (cancellationToken.IsCancellationRequested) - return; + var firstConflict = visibleUnstaged.Find(x => x.IsConflicted); + selectedUnstaged.Add(firstConflict); + } - _isLoadingData = true; - HasUnsolvedConflicts = hasConflict; - VisibleUnstaged = visibleUnstaged; - VisibleStaged = visibleStaged; - Unstaged = unstaged; - Staged = staged; - SelectedUnstaged = selectedUnstaged; - SelectedStaged = selectedStaged; - _isLoadingData = false; + _isLoadingData = true; + _cached = changes; + HasUnsolvedConflicts = hasConflict; + CanSwitchBranchDirectly = canSwitchDirectly; + VisibleUnstaged = visibleUnstaged; + VisibleStaged = visibleStaged; + Unstaged = unstaged; + Staged = staged; + SelectedUnstaged = selectedUnstaged; + SelectedStaged = selectedStaged; + _isLoadingData = false; - UpdateDetail(); - UpdateInProgressState(); - }); + UpdateInProgressState(); + UpdateDetail(); } public async Task StageChangesAsync(List changes, Models.Change next) @@ -348,22 +327,15 @@ public async Task StageChangesAsync(List changes, Models.Change n using var lockWatcher = _repo.LockWatcher(); var log = _repo.CreateLog("Stage"); - if (count == _unstaged.Count) + var pathSpecFile = Path.GetTempFileName(); + await using (var writer = new StreamWriter(pathSpecFile)) { - await new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Use(log).ExecAsync(); + foreach (var c in canStaged) + await writer.WriteLineAsync(c.Path); } - 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); - } + await new Commands.Add(_repo.FullPath, pathSpecFile).Use(log).ExecAsync(); + File.Delete(pathSpecFile); log.Complete(); _repo.MarkWorkingCopyDirtyManually(); @@ -385,7 +357,7 @@ public async Task UnstageChangesAsync(List changes, Models.Change if (_useAmend) { log.AppendLine("$ git update-index --index-info "); - await new Commands.UnstageChangesForAmend(_repo.FullPath, changes).ExecAsync(); + await new Commands.UpdateIndexInfo(_repo.FullPath, changes).ExecAsync(); } else { @@ -400,7 +372,7 @@ public async Task UnstageChangesAsync(List changes, Models.Change } } - await new Commands.Restore(_repo.FullPath, pathSpecFile, true).Use(log).ExecAsync(); + await new Commands.Reset(_repo.FullPath, pathSpecFile).Use(log).ExecAsync(); File.Delete(pathSpecFile); } log.Complete(); @@ -413,7 +385,7 @@ public async Task SaveChangesToPatchAsync(List changes, bool isUn { var succ = await Commands.SaveChangesAsPatch.ProcessLocalChangesAsync(_repo.FullPath, changes, isUnstaged, saveTo); if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); + _repo.SendNotification(App.Text("SaveAsPatchSuccess")); } public void Discard(List changes) @@ -540,7 +512,10 @@ public async Task ContinueMergeAsync() if (File.Exists(mergeMsgFile) && !string.IsNullOrWhiteSpace(_commitMessage)) await File.WriteAllTextAsync(mergeMsgFile, _commitMessage); - await _inProgressContext.ContinueAsync(); + var log = _repo.CreateLog($"Continue {_inProgressContext.Name}"); + await _inProgressContext.ContinueAsync(log); + log.Complete(); + CommitMessage = string.Empty; IsCommitting = false; } @@ -557,7 +532,10 @@ public async Task SkipMergeAsync() using var lockWatcher = _repo.LockWatcher(); IsCommitting = true; - await _inProgressContext.SkipAsync(); + var log = _repo.CreateLog($"Skip {_inProgressContext.Name}"); + await _inProgressContext.SkipAsync(log); + log.Complete(); + CommitMessage = string.Empty; IsCommitting = false; } @@ -574,7 +552,10 @@ public async Task AbortMergeAsync() using var lockWatcher = _repo.LockWatcher(); IsCommitting = true; - await _inProgressContext.AbortAsync(); + var log = _repo.CreateLog($"Abort {_inProgressContext.Name}"); + await _inProgressContext.AbortAsync(log); + log.Complete(); + CommitMessage = string.Empty; IsCommitting = false; } @@ -593,7 +574,7 @@ public async Task ClearCommitMessageHistoryAsync() { var sure = await App.AskConfirmAsync(App.Text("WorkingCopy.ClearCommitHistories.Confirm")); if (sure) - _repo.Settings.CommitMessages.Clear(); + _repo.UIStates.RecentCommitMessages.Clear(); } public async Task CommitAsync(bool autoStage, bool autoPush) @@ -603,13 +584,13 @@ public async Task CommitAsync(bool autoStage, bool autoPush) if (!_repo.CanCreatePopup()) { - App.RaiseException(_repo.FullPath, "Repository has an unfinished job! Please wait!"); + _repo.SendNotification("Repository has an unfinished job! Please wait!", true); return; } if (autoStage && HasUnsolvedConflicts) { - App.RaiseException(_repo.FullPath, "Repository has unsolved conflict(s). Auto-stage and commit is disabled!"); + _repo.SendNotification("Repository has unsolved conflict(s). Auto-stage and commit is disabled!", true); return; } @@ -633,39 +614,36 @@ public async Task CommitAsync(bool autoStage, bool autoPush) { if ((!autoStage && _staged.Count == 0) || (autoStage && _cached.Count == 0)) { - var rs = await App.AskConfirmEmptyCommitAsync(_cached.Count > 0); + var rs = await App.AskConfirmEmptyCommitAsync(_cached.Count > 0, _selectedUnstaged is { Count: > 0 }); if (rs == Models.ConfirmEmptyCommitResult.Cancel) return; if (rs == Models.ConfirmEmptyCommitResult.StageAllAndCommit) autoStage = true; + else if (rs == Models.ConfirmEmptyCommitResult.StageSelectedAndCommit) + await StageChangesAsync(_selectedUnstaged, null); } } using var lockWatcher = _repo.LockWatcher(); IsCommitting = true; - _repo.Settings.PushCommitMessage(_commitMessage); + _repo.UIStates.AddRecentCommitMessage(_commitMessage); - var log = _repo.CreateLog("Commit"); - var succ = true; if (autoStage && _unstaged.Count > 0) - succ = await new Commands.Add(_repo.FullPath, _repo.IncludeUntracked) - .Use(log) - .ExecAsync() - .ConfigureAwait(false); + await StageChangesAsync(_unstaged, null); - if (succ) - succ = await new Commands.Commit(_repo.FullPath, _commitMessage, EnableSignOff, NoVerifyOnCommit, _useAmend, _resetAuthor) + var log = _repo.CreateLog("Commit"); + var succ = await new Commands.Commit(_repo.FullPath, _commitMessage, EnableSignOff, NoVerifyOnCommit, _useAmend, _resetAuthor) .Use(log) - .RunAsync() - .ConfigureAwait(false); + .RunAsync(); log.Complete(); if (succ) { - CommitMessage = string.Empty; UseAmend = false; + CommitMessage = string.Empty; + if (autoPush && _repo.Remotes.Count > 0) { Models.Branch pushBranch = null; @@ -681,6 +659,7 @@ public async Task CommitAsync(bool autoStage, bool autoPush) } _repo.MarkBranchesDirtyManually(); + _repo.RefreshSubmodules(); // Committing will not change submodule's HEAD (stage already changes it), So we need refresh submodules here manually. IsCommitting = false; } @@ -710,15 +689,16 @@ public async Task CommitAsync(bool autoStage, bool autoPush) { if (c.IsConflicted) { - var isResolved = c.ConflictReason switch + if (c.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified) + { + var state = await new Commands.QueryConflictFileState(_repo.FullPath, c).GetResultAsync(); + if (state != Models.ConflictFileState.Resolved) + continue; + } + else { - Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified => - await new Commands.IsConflictResolved(_repo.FullPath, c).GetResultAsync(), - _ => false, - }; - - if (!isResolved) continue; + } } outs.Add(c); @@ -727,16 +707,17 @@ public async Task CommitAsync(bool autoStage, bool autoPush) return outs; } - private List GetStagedChanges() + private List GetStagedChanges(List cached) { 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 changes = new Commands.QueryStagedChangesWithAmend(_repo.FullPath).GetResult(); + changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); + return changes; } var rs = new List(); - foreach (var c in _cached) + foreach (var c in cached) { if (c.Index != Models.ChangeState.None) rs.Add(c); @@ -819,7 +800,7 @@ private bool IsChanged(List old, List cur) { var o = old[idx]; var c = cur[idx]; - if (o.Path.Equals(c.Path, StringComparison.Ordinal) || 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; } diff --git a/src/ViewModels/Worktree.cs b/src/ViewModels/Worktree.cs new file mode 100644 index 000000000..51f3deb31 --- /dev/null +++ b/src/ViewModels/Worktree.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class Worktree : ObservableObject + { + public Models.Worktree Backend { get; private set; } + public bool IsMain { get; private set; } + public bool IsCurrent { get; private set; } + public bool IsLast { get; private set; } + public string DisplayPath { get; private set; } + public string Name { get; private set; } + public string Branch { get; private set; } + + public bool IsLocked + { + get => _isLocked; + set => SetProperty(ref _isLocked, value); + } + + public string FullPath => Backend.FullPath; + public string Head => Backend.Head; + + public Worktree(DirectoryInfo repo, Models.Worktree wt, bool isMain, bool isLast) + { + Backend = wt; + IsMain = isMain; + IsCurrent = IsCurrentWorktree(repo, wt); + IsLast = isLast; + DisplayPath = IsCurrent ? string.Empty : Path.GetRelativePath(repo.FullName, wt.FullPath); + Name = GenerateName(); + Branch = GenerateBranchName(); + IsLocked = wt.IsLocked; + } + + public static List Build(string repo, List worktrees) + { + if (worktrees is not { Count: > 1 }) + return []; + + var repoDir = new DirectoryInfo(repo); + var nodes = new List(); + nodes.Add(new(repoDir, worktrees[0], true, false)); + for (int i = 1; i < worktrees.Count; i++) + nodes.Add(new(repoDir, worktrees[i], false, i == worktrees.Count - 1)); + + return nodes; + } + + public bool IsAttachedTo(Models.Branch branch) + { + if (string.IsNullOrEmpty(branch.WorktreePath)) + return false; + + var wtDir = new DirectoryInfo(Backend.FullPath); + var test = new DirectoryInfo(branch.WorktreePath); + return test.FullName.Equals(wtDir.FullName, StringComparison.Ordinal); + } + + private bool IsCurrentWorktree(DirectoryInfo repo, Models.Worktree wt) + { + var wtDir = new DirectoryInfo(wt.FullPath); + return wtDir.FullName.Equals(repo.FullName, StringComparison.Ordinal); + } + + private string GenerateName() + { + if (IsMain) + return Path.GetFileName(Backend.FullPath); + + if (Backend.IsDetached) + return $"detached HEAD at {Backend.Head.AsSpan(10)}"; + + var b = Backend.Branch; + + if (b.StartsWith("refs/heads/", StringComparison.Ordinal)) + return b.Substring(11); + + if (b.StartsWith("refs/remotes/", StringComparison.Ordinal)) + return b.Substring(13); + + return b; + } + + private string GenerateBranchName() + { + if (Backend.IsBare) + return "-- (default)"; + + if (Backend.IsDetached) + return "-- (detached)"; + + if (string.IsNullOrEmpty(Backend.Branch)) + return "-- (unknown)"; + + var b = Backend.Branch; + + if (b.StartsWith("refs/heads/", StringComparison.Ordinal)) + return b.Substring(11); + + if (b.StartsWith("refs/remotes/", StringComparison.Ordinal)) + return b.Substring(13); + + return b; + } + + private bool _isLocked = false; + } +} diff --git a/src/Views/AIAssistant.axaml b/src/Views/AIAssistant.axaml index bef13df28..9c1db462b 100644 --- a/src/Views/AIAssistant.axaml +++ b/src/Views/AIAssistant.axaml @@ -4,16 +4,16 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:v="using:SourceGit.Views" xmlns:vm="using:SourceGit.ViewModels" - mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120" + mc:Ignorable="d" d:DesignWidth="520" d:DesignHeight="400" x:Class="SourceGit.Views.AIAssistant" x:DataType="vm:AIAssistant" x:Name="ThisControl" Icon="/App.ico" Title="{DynamicResource Text.AIAssistant}" - Width="520" SizeToContent="Height" - CanResize="False" + Width="520" Height="400" + CanResize="True" WindowStartupLocation="CenterOwner"> - + - - - - @@ -70,8 +70,9 @@ - - - - + + + + + diff --git a/src/Views/About.axaml.cs b/src/Views/About.axaml.cs index e43e9aeb8..842022cf7 100644 --- a/src/Views/About.axaml.cs +++ b/src/Views/About.axaml.cs @@ -1,4 +1,6 @@ +using System; using System.Reflection; +using System.Text.RegularExpressions; using Avalonia.Interactivity; namespace SourceGit.Views @@ -11,9 +13,27 @@ public About() InitializeComponent(); var assembly = Assembly.GetExecutingAssembly(); - var ver = assembly.GetName().Version; - if (ver != null) - TxtVersion.Text = $"{ver.Major}.{ver.Minor:D2}"; + var meta = assembly.GetCustomAttributes(); + var foundFriendlyVersion = false; + foreach (var attr in meta) + { + if (attr.Key.Equals("BuildDate", StringComparison.OrdinalIgnoreCase) && DateTime.TryParse(attr.Value, out var date)) + { + TxtReleaseDate.Text = App.Text("About.ReleaseDate", Models.DateTimeFormat.Format(date, true)); + } + else if (attr.Key.Equals("FriendlyVersion", StringComparison.OrdinalIgnoreCase) && REG_FRIENDLY_VERSION().IsMatch(attr.Value)) + { + foundFriendlyVersion = true; + TxtVersion.Text = attr.Value; + } + } + + if (!foundFriendlyVersion) + { + var ver = assembly.GetName().Version; + if (ver != null) + TxtVersion.Text = $"v{ver.Major}.{ver.Minor:D2}"; + } var copyright = assembly.GetCustomAttribute(); if (copyright != null) @@ -22,7 +42,12 @@ public About() private void OnVisitReleaseNotes(object _, RoutedEventArgs e) { - Native.OS.OpenBrowser($"https://github.com/sourcegit-scm/sourcegit/releases/tag/v{TxtVersion.Text}"); + var ver = TxtVersion.Text ?? string.Empty; + var endOfTagIdx = ver.IndexOf('-'); + if (endOfTagIdx > 0) + ver = ver.Substring(0, endOfTagIdx); + + Native.OS.OpenBrowser($"https://github.com/sourcegit-scm/sourcegit/releases/tag/{ver}"); e.Handled = true; } @@ -37,5 +62,8 @@ private void OnVisitSourceCode(object _, RoutedEventArgs e) Native.OS.OpenBrowser("https://github.com/sourcegit-scm/sourcegit"); e.Handled = true; } + + [GeneratedRegex(@"^v\d{4}\.\d{1,2}(?:\-\d+\-[0-9a-f]{8})?(?:\-dirty)?$")] + private static partial Regex REG_FRIENDLY_VERSION(); } } diff --git a/src/Views/AddRemote.axaml b/src/Views/AddRemote.axaml index b9c66f48b..3903c540a 100644 --- a/src/Views/AddRemote.axaml +++ b/src/Views/AddRemote.axaml @@ -12,14 +12,14 @@ - + - - + + + Text="{Binding Name, Mode=TwoWay}"> + + + + + Text="{Binding Url, Mode=TwoWay}"> + + + + + + + + + + + - - - - - - - - - - - - + Text="{Binding File, Mode=OneWay}"/> - + @@ -122,5 +86,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index 0533beef8..e1b5fb1ff 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -38,6 +38,8 @@ public override void Render(DrawingContext context) var typeface = view.CreateTypeface(); var underlinePen = new Pen(Brushes.DarkOrange); var width = Bounds.Width; + var lineHeight = view.DefaultLineHeight; + var pixelHeight = PixelSnapHelpers.GetPixelSize(view).Height; foreach (var line in view.VisualLines) { @@ -50,8 +52,8 @@ public override void Render(DrawingContext context) var info = _editor.BlameData.LineInfos[lineNumber - 1]; var x = 0.0; - var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; - if (!info.IsFirstInGroup && y > view.DefaultLineHeight * 0.6) + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineMiddle) - view.VerticalOffset; + if (!info.IsFirstInGroup && y > lineHeight) continue; var shaLink = new FormattedText( @@ -61,8 +63,10 @@ public override void Render(DrawingContext context) typeface, _editor.FontSize, Brushes.DarkOrange); - context.DrawText(shaLink, new Point(x, y)); - context.DrawLine(underlinePen, new Point(x, y + shaLink.Baseline + 2), new Point(x + shaLink.Width, y + shaLink.Baseline + 2)); + var shaLinkTop = y - shaLink.Height * 0.5; + var underlineY = PixelSnapHelpers.PixelAlign(y + shaLink.Height * 0.5 + 0.5, pixelHeight); + context.DrawText(shaLink, new Point(x, shaLinkTop)); + context.DrawLine(underlinePen, new Point(x, underlineY), new Point(x + shaLink.Width, underlineY)); x += shaLink.Width + 8; var author = new FormattedText( @@ -72,16 +76,19 @@ public override void Render(DrawingContext context) typeface, _editor.FontSize, _editor.Foreground); - context.DrawText(author, new Point(x, y)); + var authorTop = y - author.Height * 0.5; + context.DrawText(author, new Point(x, authorTop)); + var timeStr = Models.DateTimeFormat.Format(info.Timestamp, true); var time = new FormattedText( - info.Time, + timeStr, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, _editor.Foreground); - context.DrawText(time, new Point(width - time.Width, y)); + var timeTop = y - time.Height * 0.5; + context.DrawText(time, new Point(width - time.Width, timeTop)); } } } @@ -124,8 +131,9 @@ protected override Size MeasureOverride(Size availableSize) _editor.Foreground); x += author.Width + 8; + var timeStr = Models.DateTimeFormat.Format(info.Timestamp, true); var time = new FormattedText( - info.Time, + timeStr, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, @@ -150,6 +158,7 @@ protected override void OnPointerMoved(PointerEventArgs e) { var pos = e.GetPosition(this); var typeface = view.CreateTypeface(); + var lineHeight = view.DefaultLineHeight; foreach (var line in view.VisualLines) { @@ -161,7 +170,7 @@ protected override void OnPointerMoved(PointerEventArgs e) break; var info = _editor.BlameData.LineInfos[lineNumber - 1]; - var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - view.VerticalOffset; var shaLink = new FormattedText( info.CommitSHA, CultureInfo.CurrentCulture, @@ -170,7 +179,7 @@ protected override void OnPointerMoved(PointerEventArgs e) _editor.FontSize, Brushes.DarkOrange); - var rect = new Rect(0, y, shaLink.Width, shaLink.Height); + var rect = new Rect(0, y, shaLink.Width, lineHeight); if (rect.Contains(pos)) { Cursor = Cursor.Parse("Hand"); @@ -223,7 +232,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) if (rect.Contains(pos)) { if (DataContext is ViewModels.Blame blame) - blame.NavigateToCommit(info.CommitSHA, false); + blame.NavigateToCommit(info.File, info.CommitSHA); e.Handled = true; break; @@ -245,7 +254,7 @@ public VerticalSeparatorMargin(BlameTextEditor editor) public override void Render(DrawingContext context) { var pen = new Pen(_editor.BorderBrush); - context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height)); + context.DrawLine(pen, new Point(0.5, 0), new Point(0.5, Bounds.Height)); } protected override Size MeasureOverride(Size availableSize) @@ -256,22 +265,88 @@ protected override Size MeasureOverride(Size availableSize) private readonly BlameTextEditor _editor = null; } - public static readonly StyledProperty BlameDataProperty = - AvaloniaProperty.Register(nameof(BlameData)); + public class LineBackgroundRenderer : IBackgroundRenderer + { + public KnownLayer Layer => KnownLayer.Background; + + public LineBackgroundRenderer(BlameTextEditor owner) + { + _owner = owner; + } + + public void Draw(TextView textView, DrawingContext drawingContext) + { + if (!textView.VisualLinesValid) + return; + + var w = textView.Bounds.Width; + if (double.IsNaN(w) || double.IsInfinity(w) || w <= 0) + return; + + var highlight = _owner._highlight; + if (string.IsNullOrEmpty(highlight)) + return; + + var color = (Color)_owner.FindResource("SystemAccentColor")!; + var brush = new SolidColorBrush(color, 0.2); + var lines = _owner.BlameData.LineInfos; + + foreach (var line in textView.VisualLines) + { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + + var lineNumber = line.FirstDocumentLine.LineNumber; + if (lineNumber > lines.Count) + break; + + var info = lines[lineNumber - 1]; + if (!info.CommitSHA.Equals(highlight, StringComparison.Ordinal)) + continue; + + var startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - textView.VerticalOffset; + var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - textView.VerticalOffset; + drawingContext.FillRectangle(brush, new Rect(0, startY, w, endY - startY)); + } + } + + private readonly BlameTextEditor _owner; + } + + public static readonly DirectProperty FileProperty = + AvaloniaProperty.RegisterDirect( + nameof(File), + static o => o.File, + static (o, v) => o.File = v); + + public string File + { + get => _file; + set => SetAndRaise(FileProperty, ref _file, value); + } + + public static readonly DirectProperty BlameDataProperty = + AvaloniaProperty.RegisterDirect( + nameof(BlameData), + static o => o.BlameData, + static (o, v) => o.BlameData = v); public Models.BlameData BlameData { - get => GetValue(BlameDataProperty); - set => SetValue(BlameDataProperty, value); + get => _blameData; + set => SetAndRaise(BlameDataProperty, ref _blameData, value); } - public static readonly StyledProperty TabWidthProperty = - AvaloniaProperty.Register(nameof(TabWidth), 4); + public static readonly DirectProperty TabWidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(TabWidth), + static o => o.TabWidth, + static (o, v) => o.TabWidth = v); public int TabWidth { - get => GetValue(TabWidthProperty); - set => SetValue(TabWidthProperty, value); + get => _tabWidth; + set => SetAndRaise(TabWidthProperty, ref _tabWidth, value); } protected override Type StyleKeyOverride => typeof(TextEditor); @@ -282,7 +357,7 @@ public int TabWidth ShowLineNumbers = false; WordWrap = false; - Options.IndentationSize = TabWidth; + Options.IndentationSize = _tabWidth; Options.EnableHyperlinks = false; Options.EnableEmailHyperlinks = false; @@ -293,43 +368,12 @@ public int TabWidth TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) }); TextArea.LeftMargins.Add(new VerticalSeparatorMargin(this)); TextArea.Caret.PositionChanged += OnTextAreaCaretPositionChanged; + TextArea.TextView.BackgroundRenderers.Add(new LineBackgroundRenderer(this)); TextArea.TextView.ContextRequested += OnTextViewContextRequested; TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged; TextArea.TextView.Margin = new Thickness(4, 0); } - public override void Render(DrawingContext context) - { - base.Render(context); - - if (string.IsNullOrEmpty(_highlight)) - return; - - var view = TextArea.TextView; - if (view is not { VisualLinesValid: true }) - return; - - var color = (Color)this.FindResource("SystemAccentColor")!; - var brush = new SolidColorBrush(color, 0.4); - foreach (var line in view.VisualLines) - { - if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) - continue; - - var lineNumber = line.FirstDocumentLine.LineNumber; - if (lineNumber > BlameData.LineInfos.Count) - break; - - var info = BlameData.LineInfos[lineNumber - 1]; - if (info.CommitSHA != _highlight) - continue; - - var startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - view.VerticalOffset; - var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - view.VerticalOffset; - context.FillRectangle(brush, new Rect(0, startY, Bounds.Width, endY - startY)); - } - } - protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); @@ -350,23 +394,23 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { base.OnPropertyChanged(change); + if (change.Property == FileProperty) + { + if (_file is { Length: > 0 }) + Models.TextMateHelper.SetGrammarByFileName(_textMate, _file); + } if (change.Property == BlameDataProperty) { - if (BlameData is { IsBinary: false } blame) - { - Models.TextMateHelper.SetGrammarByFileName(_textMate, blame.File); + if (_blameData is { IsBinary: false } blame) Text = blame.Content; - } else - { Text = string.Empty; - } } else if (change.Property == TabWidthProperty) { - Options.IndentationSize = TabWidth; + Options.IndentationSize = _tabWidth; } - else if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null) + else if (change.Property.Name == nameof(ActualThemeVariant) && change.NewValue != null) { Models.TextMateHelper.SetThemeByApp(_textMate); } @@ -374,15 +418,14 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang private void OnTextAreaCaretPositionChanged(object sender, EventArgs e) { - if (!TextArea.IsFocused) + if (!TextArea.IsFocused || _blameData == null) return; var caret = TextArea.Caret; - if (caret == null || caret.Line > BlameData.LineInfos.Count) + if (caret == null || caret.Line > _blameData.LineInfos.Count) return; - _highlight = BlameData.LineInfos[caret.Line - 1].CommitSHA; - InvalidateVisual(); + _highlight = _blameData.LineInfos[caret.Line - 1].CommitSHA; } private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) @@ -393,10 +436,10 @@ private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs var copy = new MenuItem(); copy.Header = App.Text("Copy"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Icon = this.CreateMenuIcon("Icons.Copy"); copy.Click += async (_, ev) => { - await App.CopyTextAsync(selected); + await this.CopyTextAsync(selected); ev.Handled = true; }; @@ -417,10 +460,11 @@ private void OnTextViewVisualLinesChanged(object sender, EventArgs e) break; } } - - InvalidateVisual(); } + private string _file = null; + private Models.BlameData _blameData = null; + private int _tabWidth = 4; private TextMate.Installation _textMate = null; private string _highlight = string.Empty; } diff --git a/src/Views/BlameCommandPalette.axaml b/src/Views/BlameCommandPalette.axaml index b20ffa519..efb2000f8 100644 --- a/src/Views/BlameCommandPalette.axaml +++ b/src/Views/BlameCommandPalette.axaml @@ -8,7 +8,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.BlameCommandPalette" x:DataType="vm:BlameCommandPalette"> - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchSelector.axaml.cs b/src/Views/BranchSelector.axaml.cs new file mode 100644 index 000000000..4cd2b70d5 --- /dev/null +++ b/src/Views/BranchSelector.axaml.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class BranchSelectorChoice : TextBlock + { + public static readonly DirectProperty BranchProperty = + AvaloniaProperty.RegisterDirect( + nameof(Branch), + static o => o.Branch, + static (o, v) => o.Branch = v); + + public Models.Branch Branch + { + get => _branch; + set => SetAndRaise(BranchProperty, ref _branch, value); + } + + public static readonly DirectProperty UsePureNameProperty = + AvaloniaProperty.RegisterDirect( + nameof(UsePureName), + static o => o.UsePureName, + static (o, v) => o.UsePureName = v); + + public bool UsePureName + { + get => _usePureName; + set => SetAndRaise(UsePureNameProperty, ref _usePureName, value); + } + + protected override Type StyleKeyOverride => typeof(TextBlock); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BranchProperty || change.Property == UsePureNameProperty) + { + if (_branch != null) + Text = _usePureName ? _branch.Name : _branch.FriendlyName; + else + Text = "---"; + } + } + + private Models.Branch _branch = null; + private bool _usePureName = false; + } + + public partial class BranchSelector : UserControl + { + public static readonly DirectProperty> BranchesProperty = + AvaloniaProperty.RegisterDirect>( + nameof(Branches), + static o => o.Branches, + static (o, v) => o.Branches = v); + + public List Branches + { + get => _branches; + set => SetAndRaise(BranchesProperty, ref _branches, value); + } + + public static readonly DirectProperty> VisibleBranchesProperty = + AvaloniaProperty.RegisterDirect>( + nameof(VisibleBranches), + static o => o.VisibleBranches); + + public List VisibleBranches + { + get => _visibleBranches; + set => SetAndRaise(VisibleBranchesProperty, ref _visibleBranches, value); + } + + public static readonly DirectProperty SelectedBranchProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedBranch), + static o => o.SelectedBranch, + static (o, v) => o.SelectedBranch = v); + + public Models.Branch SelectedBranch + { + get => _selectedBranch; + set => SetAndRaise(SelectedBranchProperty, ref _selectedBranch, value); + } + + public static readonly DirectProperty IsDropDownOpenedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsDropDownOpened), + static o => o.IsDropDownOpened, + static (o, v) => o.IsDropDownOpened = v); + + public bool IsDropDownOpened + { + get => _isDropDownOpened; + set => SetAndRaise(IsDropDownOpenedProperty, ref _isDropDownOpened, value); + } + + public static readonly DirectProperty SearchFilterProperty = + AvaloniaProperty.RegisterDirect( + nameof(SearchFilter), + static o => o.SearchFilter, + static (o, v) => o.SearchFilter = v); + + public string SearchFilter + { + get => _searchFilter; + set => SetAndRaise(SearchFilterProperty, ref _searchFilter, value); + } + + public static readonly DirectProperty UsePureNameProperty = + AvaloniaProperty.RegisterDirect( + nameof(UsePureName), + static o => o.UsePureName, + static (o, v) => o.UsePureName = v); + + public bool UsePureName + { + get => _usePureName; + set => SetAndRaise(UsePureNameProperty, ref _usePureName, value); + } + + public BranchSelector() + { + Focusable = true; + InitializeComponent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BranchesProperty || change.Property == SearchFilterProperty) + { + if (_branches is not { Count: > 0 }) + { + VisibleBranches = []; + } + else if (string.IsNullOrEmpty(_searchFilter)) + { + VisibleBranches = _branches; + } + else + { + var visible = new List(); + var oldSelection = _selectedBranch; + var keepSelection = false; + + foreach (var b in _branches) + { + if (b.FriendlyName.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + { + visible.Add(b); + if (!keepSelection) + keepSelection = (b == oldSelection); + } + } + + VisibleBranches = visible; + if (!keepSelection && visible.Count > 0) + SelectedBranch = visible[0]; + } + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (_popup != null) + { + _popup.Opened -= OnPopupOpened; + _popup.Closed -= OnPopupClosed; + } + + _popup = e.NameScope.Get("PART_Popup"); + _popup.Opened += OnPopupOpened; + _popup.Closed += OnPopupClosed; + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Space && !IsDropDownOpened) + { + IsDropDownOpened = true; + e.Handled = true; + } + else if (e.Key == Key.Escape && IsDropDownOpened) + { + IsDropDownOpened = false; + e.Handled = true; + } + } + + private void OnPopupOpened(object sender, EventArgs e) + { + var listBox = _popup?.Child?.FindDescendantOfType(); + listBox?.Focus(); + } + + private void OnPopupClosed(object sender, EventArgs e) + { + Focus(NavigationMethod.Directional); + } + + private void OnToggleDropDown(object sender, PointerPressedEventArgs e) + { + IsDropDownOpened = !IsDropDownOpened; + e.Handled = true; + } + + private void OnSearchBoxKeyDown(object _, KeyEventArgs e) + { + if (e.Key == Key.Tab) + { + var listBox = _popup?.Child?.FindDescendantOfType(); + listBox?.Focus(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + var listBox = _popup?.Child?.FindDescendantOfType(); + if (listBox != null) + { + if (listBox.SelectedIndex > 0) + listBox.SelectedIndex--; + listBox.Focus(); + } + + e.Handled = true; + } + else if (e.Key == Key.Down) + { + var listBox = _popup?.Child?.FindDescendantOfType(); + if (listBox != null) + { + if (listBox.SelectedIndex < listBox.Items.Count - 1) + listBox.SelectedIndex++; + listBox.Focus(); + } + + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + IsDropDownOpened = false; + e.Handled = true; + } + } + + private void OnClearSearchFilter(object sender, RoutedEventArgs e) + { + SearchFilter = string.Empty; + e.Handled = true; + } + + private void OnDropDownListKeyDown(object _, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + IsDropDownOpened = false; + e.Handled = true; + } + else if (e.Key == Key.F && e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) + { + var searchBox = _popup?.Child?.FindDescendantOfType(); + if (searchBox != null) + { + searchBox.CaretIndex = SearchFilter?.Length ?? 0; + searchBox.Focus(); + } + + e.Handled = true; + } + } + + private void OnDropDownListSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.AddedItems.Count == 1 && e.AddedItems[0] is Models.Branch branch) + SelectedBranch = branch; + } + + private void OnDropDownItemPointerPressed(object sender, PointerPressedEventArgs e) + { + if (sender is Control { DataContext: Models.Branch branch }) + SelectedBranch = branch; + + IsDropDownOpened = false; + e.Handled = true; + } + + private List _branches; + private List _visibleBranches; + private bool _isDropDownOpened; + private string _searchFilter = string.Empty; + private bool _usePureName = false; + private Popup _popup = null; + private Models.Branch _selectedBranch = null; + } +} diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index 0d122d606..98ded57a1 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"> @@ -28,7 +29,7 @@ @@ -111,7 +112,7 @@ - + @@ -126,12 +127,16 @@ - + - - diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index fd14fc3a2..1c3cfdc3c 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -147,18 +147,6 @@ public IBrush Background set => SetValue(BackgroundProperty, value); } - static BranchTreeNodeTrackStatusPresenter() - { - AffectsMeasure( - FontSizeProperty, - FontFamilyProperty, - ForegroundProperty); - - AffectsRender( - ForegroundProperty, - BackgroundProperty); - } - public override void Render(DrawingContext context) { base.Render(context); @@ -170,6 +158,18 @@ public override void Render(DrawingContext context) } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FontSizeProperty || + change.Property == FontFamilyProperty || + change.Property == ForegroundProperty) + InvalidateMeasure(); + else if (change.Property == BackgroundProperty) + InvalidateVisual(); + } + protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); @@ -270,20 +270,22 @@ protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventAr public partial class BranchTree : UserControl { - public static readonly StyledProperty> NodesProperty = - AvaloniaProperty.Register>(nameof(Nodes)); + public static readonly DirectProperty> NodesProperty = + AvaloniaProperty.RegisterDirect>( + nameof(Nodes), + static o => o.Nodes, + static (o, v) => o.Nodes = v); public List Nodes { - get => GetValue(NodesProperty); - set => SetValue(NodesProperty, value); + get => _nodes; + set => SetAndRaise(NodesProperty, ref _nodes, value); } public AvaloniaList Rows { get; - private set; - } = new AvaloniaList(); + } = []; public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); @@ -303,6 +305,15 @@ public event EventHandler RowsChanged remove { RemoveHandler(RowsChangedEvent, value); } } + public static readonly RoutedEvent SearchRequestedEvent = + RoutedEvent.Register(nameof(SearchRequested), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler SearchRequested + { + add { AddHandler(SearchRequestedEvent, value); } + remove { RemoveHandler(SearchRequestedEvent, value); } + } + public BranchTree() { InitializeComponent(); @@ -314,7 +325,7 @@ public void Select(Models.Branch branch) return; var treePath = new List(); - FindTreePath(treePath, Nodes, branch.Name, 0); + FindTreePath(treePath, _nodes, branch.Name, 0); if (treePath.Count == 0) return; @@ -403,10 +414,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { Rows.Clear(); - if (Nodes is { Count: > 0 }) + if (_nodes is { Count: > 0 }) { var rows = new List(); - MakeRows(rows, Nodes, 0); + MakeRows(rows, _nodes, 0); Rows.AddRange(rows); } @@ -517,71 +528,113 @@ private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2) var menu = branch.IsLocal ? CreateContextMenuForLocalBranch(repo, branch) : CreateContextMenuForRemoteBranch(repo, branch); menu.Open(this); } - else if (branches.Find(x => x.IsCurrent) == null) + else { var menu = new ContextMenu(); - var mergeMulti = new MenuItem(); - mergeMulti.Header = App.Text("BranchCM.MergeMultiBranches", branches.Count); - mergeMulti.Icon = App.CreateMenuIcon("Icons.Merge"); - mergeMulti.Click += (_, ev) => + if (branches.Count == 2) { - repo.MergeMultipleBranches(branches); - ev.Handled = true; - }; - menu.Items.Add(mergeMulti); - menu.Items.Add(new MenuItem() { Header = "-" }); + var compare = new MenuItem(); + compare.Header = App.Text("BranchCM.CompareTwo"); + compare.Icon = this.CreateMenuIcon("Icons.Compare"); + compare.Click += (_, ev) => + { + this.ShowWindow(new ViewModels.Compare(repo, branches[0], branches[1])); + ev.Handled = true; + }; + menu.Items.Add(compare); + } - var deleteMulti = new MenuItem(); - deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); - deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); - deleteMulti.Click += (_, ev) => + if (branches.Find(x => x.IsCurrent) == null) { - repo.DeleteMultipleBranches(branches, branches[0].IsLocal); - ev.Handled = true; - }; - menu.Items.Add(deleteMulti); + var mergeMulti = new MenuItem(); + mergeMulti.Header = App.Text("BranchCM.MergeMultiBranches", branches.Count); + mergeMulti.Icon = this.CreateMenuIcon("Icons.Merge"); + mergeMulti.Click += (_, ev) => + { + repo.MergeMultipleBranches(branches); + ev.Handled = true; + }; - menu.Open(this); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = this.CreateMenuIcon("Icons.Clear"); + deleteMulti.Tag = "Delete/Back"; + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, branches[0].IsLocal); + ev.Handled = true; + }; + + menu.Items.Add(mergeMulti); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(deleteMulti); + } + + if (menu.Items.Count > 0) + menu.Open(this); } } private void OnTreeKeyDown(object _, KeyEventArgs e) { - if (e.Key is not (Key.Delete or Key.Back)) - return; + if (e.Key == Key.F && e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) + { + RaiseEvent(new RoutedEventArgs(SearchRequestedEvent)); + e.Handled = true; + } + else if ((e.Key == Key.Delete || e.Key == Key.Back) && e.KeyModifiers == KeyModifiers.None) + { + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; - var repo = DataContext as ViewModels.Repository; - if (repo?.Settings == null) - return; + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; - var selected = BranchesPresenter.SelectedItems; - if (selected == null || selected.Count == 0) - return; + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) + { + repo.DeleteRemote(remote); + e.Handled = true; + return; + } + + var branches = new List(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + CollectBranchesInNode(branches, node); + } + + if (branches.Find(x => x.IsCurrent) != null) + return; + + if (branches.Count == 1) + repo.DeleteBranch(branches[0]); + else + repo.DeleteMultipleBranches(branches, branches[0].IsLocal); - if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) - { - repo.DeleteRemote(remote); e.Handled = true; - return; } - - var branches = new List(); - foreach (var item in selected) + else if (e.Key == Key.F2 && e.KeyModifiers == KeyModifiers.None) { - if (item is ViewModels.BranchTreeNode node) - CollectBranchesInNode(branches, node); - } + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; - if (branches.Find(x => x.IsCurrent) != null) - return; + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count != 1) + return; - if (branches.Count == 1) - repo.DeleteBranch(branches[0]); - else - repo.DeleteMultipleBranches(branches, branches[0].IsLocal); + if (selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch { IsLocal: true } branch }) + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RenameBranch(repo, branch)); - e.Handled = true; + e.Handled = true; + } + } } private async void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) @@ -655,7 +708,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, var push = new MenuItem(); push.Header = App.Text("BranchCM.Push", branch.Name); - push.Icon = App.CreateMenuIcon("Icons.Push"); + push.Icon = this.CreateMenuIcon("Icons.Push"); push.IsEnabled = repo.Remotes.Count > 0; push.Click += (_, e) => { @@ -672,7 +725,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, { var fastForward = new MenuItem(); fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName); - fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); + fastForward.Icon = this.CreateMenuIcon("Icons.FastForward"); fastForward.IsEnabled = branch.Ahead.Count == 0 && branch.Behind.Count > 0; fastForward.Click += async (_, e) => { @@ -683,7 +736,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, var pull = new MenuItem(); pull.Header = App.Text("BranchCM.Pull", upstream.FriendlyName); - pull.Icon = App.CreateMenuIcon("Icons.Pull"); + pull.Icon = this.CreateMenuIcon("Icons.Pull"); pull.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -698,6 +751,45 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, } menu.Items.Add(push); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var type = repo.GetGitFlowType(branch); + if (type != Models.GitFlowBranchType.None) + { + var finish = new MenuItem(); + finish.Header = App.Text("BranchCM.Finish", branch.Name); + finish.Icon = this.CreateMenuIcon("Icons.GitFlow.Finish"); + finish.IsEnabled = !repo.IsBare; + finish.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.GitFlowFinish(repo, branch, type)); + e.Handled = true; + }; + menu.Items.Add(finish); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + if (upstream != null) + { + var compareWithUpstream = new MenuItem(); + compareWithUpstream.Header = App.Text("BranchCM.CompareWithSpecial", upstream.FriendlyName); + compareWithUpstream.Icon = this.CreateMenuIcon("Icons.Compare"); + compareWithUpstream.Click += (_, _) => + { + this.ShowWindow(new ViewModels.Compare(repo, upstream, branch)); + }; + menu.Items.Add(compareWithUpstream); + } + + var compareWith = new MenuItem(); + compareWith.Header = App.Text("BranchCM.CompareWith"); + compareWith.Icon = this.CreateMenuIcon("Icons.Compare"); + compareWith.Click += (_, _) => + { + new ViewModels.CompareCommandPalette(repo, branch).Open(); + }; + menu.Items.Add(compareWith); } else { @@ -705,7 +797,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, var checkout = new MenuItem(); checkout.Header = App.Text(hasNoWorktree ? "BranchCM.Checkout" : "BranchCM.SwitchToWorktree", branch.Name); - checkout.Icon = App.CreateMenuIcon("Icons.Check"); + checkout.Icon = this.CreateMenuIcon("Icons.Check"); checkout.IsEnabled = !repo.IsBare || !hasNoWorktree; checkout.Click += async (_, e) => { @@ -719,7 +811,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, { var fastForward = new MenuItem(); fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName); - fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); + fastForward.Icon = this.CreateMenuIcon("Icons.FastForward"); fastForward.IsEnabled = branch.Ahead.Count == 0 && branch.Behind.Count > 0; fastForward.Click += async (_, e) => { @@ -731,7 +823,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, var fetchInto = new MenuItem(); fetchInto.Header = App.Text("BranchCM.FetchInto", upstream.FriendlyName, branch.Name); - fetchInto.Icon = App.CreateMenuIcon("Icons.Fetch"); + fetchInto.Icon = this.CreateMenuIcon("Icons.Fetch"); fetchInto.IsEnabled = branch.Ahead.Count == 0; fetchInto.Click += async (_, e) => { @@ -750,7 +842,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, { var merge = new MenuItem(); merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Icon = this.CreateMenuIcon("Icons.Merge"); merge.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -760,7 +852,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, var rebase = new MenuItem(); rebase.Header = App.Text("BranchCM.Rebase", current.Name, branch.Name); - rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); + rebase.Icon = this.CreateMenuIcon("Icons.Rebase"); rebase.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -768,8 +860,38 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, e.Handled = true; }; + var interactiveRebase = new MenuItem(); + interactiveRebase.Header = App.Text("BranchCM.InteractiveRebase.Manually", current.Name, branch.Name); + interactiveRebase.Icon = this.CreateMenuIcon("Icons.InteractiveRebase"); + interactiveRebase.IsEnabled = !current.Head.Equals(branch.Head, StringComparison.Ordinal); + interactiveRebase.Click += async (_, e) => + { + var commit = await new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResultAsync(); + await this.ShowDialogAsync(new ViewModels.InteractiveRebase(repo, commit)); + e.Handled = true; + }; + menu.Items.Add(merge); menu.Items.Add(rebase); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(interactiveRebase); + + var type = repo.GetGitFlowType(branch); + if (type != Models.GitFlowBranchType.None) + { + var finish = new MenuItem(); + finish.Header = App.Text("BranchCM.Finish", branch.Name); + finish.Icon = this.CreateMenuIcon("Icons.GitFlow.Finish"); + finish.IsEnabled = !repo.IsBare || !hasNoWorktree; + 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); + } } if (hasNoWorktree) @@ -779,7 +901,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, { var move = new MenuItem(); move.Header = App.Text("BranchCM.ResetToSelectedCommit", branch.Name, selectedCommit.SHA.Substring(0, 10)); - move.Icon = App.CreateMenuIcon("Icons.Reset"); + move.Icon = this.CreateMenuIcon("Icons.Reset"); move.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -791,83 +913,83 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, } } + menu.Items.Add(new MenuItem() { Header = "-" }); + var compareWithCurrent = new MenuItem(); - compareWithCurrent.Header = App.Text("BranchCM.CompareWithCurrent", current.Name); - compareWithCurrent.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithCurrent.Header = App.Text("BranchCM.CompareWithHead"); + compareWithCurrent.Icon = this.CreateMenuIcon("Icons.Compare"); compareWithCurrent.Click += (_, _) => { - App.ShowWindow(new ViewModels.BranchCompare(repo.FullPath, branch, current)); + this.ShowWindow(new ViewModels.Compare(repo, branch, current)); }; - menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(compareWithCurrent); - if (repo.LocalChangesCount > 0) + if (upstream != null) { - var compareWithWorktree = new MenuItem(); - compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); - compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithWorktree.Click += async (_, e) => + var compareWithUpstream = new MenuItem(); + compareWithUpstream.Header = App.Text("BranchCM.CompareWithSpecial", upstream.FriendlyName); + compareWithUpstream.Icon = this.CreateMenuIcon("Icons.Compare"); + compareWithUpstream.Click += (_, _) => { - await repo.CompareBranchWithWorktreeAsync(branch); - e.Handled = true; + this.ShowWindow(new ViewModels.Compare(repo, upstream, branch)); }; - menu.Items.Add(compareWithWorktree); + menu.Items.Add(compareWithUpstream); } - } - if (!repo.IsBare) - { - var type = repo.GetGitFlowType(branch); - if (type != Models.GitFlowBranchType.None) + var compareWith = new MenuItem(); + compareWith.Header = App.Text("BranchCM.CompareWith"); + compareWith.Icon = this.CreateMenuIcon("Icons.Compare"); + compareWith.Click += (_, _) => { - 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); - } + new ViewModels.CompareCommandPalette(repo, branch).Open(); + }; + menu.Items.Add(compareWith); } - var rename = new MenuItem(); - rename.Header = App.Text("BranchCM.Rename", branch.Name); - rename.Icon = App.CreateMenuIcon("Icons.Rename"); - rename.Click += (_, e) => + if (!branch.IsDetachedHead) { - 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 = this.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 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 rename = new MenuItem(); + rename.Header = App.Text("BranchCM.Rename", branch.Name); + rename.Icon = this.CreateMenuIcon("Icons.Rename"); + rename.Tag = "F2"; + rename.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RenameBranch(repo, 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 (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); - e.Handled = true; - }; + var delete = new MenuItem(); + delete.Header = App.Text("BranchCM.Delete", branch.Name); + delete.Icon = this.CreateMenuIcon("Icons.Clear"); + delete.Tag = "Delete/Back"; + delete.IsEnabled = !branch.IsCurrent; + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); + e.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(editDescription); + menu.Items.Add(rename); + menu.Items.Add(delete); + } var createBranch = new MenuItem(); - createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); + createBranch.Icon = this.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); createBranch.Click += (_, e) => { @@ -877,7 +999,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, }; var createTag = new MenuItem(); - createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); + createTag.Icon = this.CreateMenuIcon("Icons.Tag.Add"); createTag.Header = App.Text("CreateTag"); createTag.Click += (_, e) => { @@ -886,10 +1008,6 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, 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); @@ -901,7 +1019,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, { var createPR = new MenuItem(); createPR.Header = App.Text("BranchCM.CreatePRForUpstream", upstream.FriendlyName); - createPR.Icon = App.CreateMenuIcon("Icons.CreatePR"); + createPR.Icon = this.CreateMenuIcon("Icons.CreatePR"); createPR.Click += (_, e) => { Native.OS.OpenBrowser(prURL); @@ -927,7 +1045,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, { var tracking = new MenuItem(); tracking.Header = App.Text("BranchCM.Tracking"); - tracking.Icon = App.CreateMenuIcon("Icons.Track"); + tracking.Icon = this.CreateMenuIcon("Icons.Track"); tracking.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -939,7 +1057,7 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, } var archive = new MenuItem(); - archive.Icon = App.CreateMenuIcon("Icons.Archive"); + archive.Icon = this.CreateMenuIcon("Icons.Archive"); archive.Header = App.Text("Archive"); archive.Click += (_, e) => { @@ -952,10 +1070,10 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, var copy = new MenuItem(); copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Icon = this.CreateMenuIcon("Icons.Copy"); copy.Click += async (_, e) => { - await App.CopyTextAsync(branch.Name); + await this.CopyTextAsync(branch.Name); e.Handled = true; }; menu.Items.Add(copy); @@ -971,7 +1089,7 @@ private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Model { var visit = new MenuItem(); visit.Header = App.Text("RemoteCM.OpenInBrowser"); - visit.Icon = App.CreateMenuIcon("Icons.OpenWith"); + visit.Icon = this.CreateMenuIcon("Icons.OpenWith"); visit.Click += (_, e) => { Native.OS.OpenBrowser(visitURL); @@ -984,7 +1102,7 @@ private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Model var fetch = new MenuItem(); fetch.Header = App.Text("RemoteCM.Fetch"); - fetch.Icon = App.CreateMenuIcon("Icons.Fetch"); + fetch.Icon = this.CreateMenuIcon("Icons.Fetch"); fetch.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -994,7 +1112,7 @@ private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Model var prune = new MenuItem(); prune.Header = App.Text("RemoteCM.Prune"); - prune.Icon = App.CreateMenuIcon("Icons.Clean"); + prune.Icon = this.CreateMenuIcon("Icons.Clean"); prune.Click += async (_, e) => { if (repo.CanCreatePopup()) @@ -1002,9 +1120,29 @@ private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Model e.Handled = true; }; + menu.Items.Add(fetch); + menu.Items.Add(prune); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (ViewModels.Preferences.Instance.EnableAutoFetch) + { + var toggleAutoFetch = new MenuItem(); + toggleAutoFetch.Header = App.Text("RemoteCM.EnableAutoFetch"); + toggleAutoFetch.Icon = this.CreateMenuIcon("Icons.AutoFetch"); + toggleAutoFetch.Tag = remote.DisableAutoFetch ? "OFF" : "ON"; + toggleAutoFetch.Click += async (_, e) => + { + await repo.ToggleAutoFetchOnRemoteAsync(remote); + e.Handled = true; + }; + + menu.Items.Add(toggleAutoFetch); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + var edit = new MenuItem(); edit.Header = App.Text("RemoteCM.Edit"); - edit.Icon = App.CreateMenuIcon("Icons.Edit"); + edit.Icon = this.CreateMenuIcon("Icons.Edit"); edit.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -1014,7 +1152,7 @@ private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Model var delete = new MenuItem(); delete.Header = App.Text("RemoteCM.Delete"); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Icon = this.CreateMenuIcon("Icons.Clear"); delete.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -1024,16 +1162,13 @@ private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Model var copy = new MenuItem(); copy.Header = App.Text("RemoteCM.CopyURL"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Icon = this.CreateMenuIcon("Icons.Copy"); copy.Click += async (_, e) => { - await App.CopyTextAsync(remote.URL); + await this.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 = "-" }); @@ -1049,7 +1184,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, var checkout = new MenuItem(); checkout.Header = App.Text("BranchCM.Checkout", name); - checkout.Icon = App.CreateMenuIcon("Icons.Check"); + checkout.Icon = this.CreateMenuIcon("Icons.Check"); checkout.Click += async (_, e) => { await repo.CheckoutBranchAsync(branch); @@ -1062,7 +1197,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, { var pull = new MenuItem(); pull.Header = App.Text("BranchCM.PullInto", name, current.Name); - pull.Icon = App.CreateMenuIcon("Icons.Pull"); + pull.Icon = this.CreateMenuIcon("Icons.Pull"); pull.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -1072,7 +1207,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, var merge = new MenuItem(); merge.Header = App.Text("BranchCM.Merge", name, current.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Icon = this.CreateMenuIcon("Icons.Merge"); merge.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -1082,7 +1217,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, var rebase = new MenuItem(); rebase.Header = App.Text("BranchCM.Rebase", current.Name, name); - rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); + rebase.Icon = this.CreateMenuIcon("Icons.Rebase"); rebase.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -1090,38 +1225,48 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, e.Handled = true; }; - menu.Items.Add(pull); - menu.Items.Add(merge); - menu.Items.Add(rebase); - menu.Items.Add(new MenuItem() { Header = "-" }); + var interactiveRebase = new MenuItem(); + interactiveRebase.Header = App.Text("BranchCM.InteractiveRebase.Manually", current.Name, name); + interactiveRebase.Icon = this.CreateMenuIcon("Icons.InteractiveRebase"); + interactiveRebase.IsEnabled = !current.Head.Equals(branch.Head, StringComparison.Ordinal); + interactiveRebase.Click += async (_, e) => + { + var commit = await new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResultAsync(); + await this.ShowDialogAsync(new ViewModels.InteractiveRebase(repo, commit)); + e.Handled = true; + }; var compareWithHead = new MenuItem(); - compareWithHead.Header = App.Text("BranchCM.CompareWithCurrent", current.Name); - compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithHead.Header = App.Text("BranchCM.CompareWithHead"); + compareWithHead.Icon = this.CreateMenuIcon("Icons.Compare"); compareWithHead.Click += (_, _) => { - App.ShowWindow(new ViewModels.BranchCompare(repo.FullPath, branch, current)); + this.ShowWindow(new ViewModels.Compare(repo, 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) => + var compareWith = new MenuItem(); + compareWith.Header = App.Text("BranchCM.CompareWith"); + compareWith.Icon = this.CreateMenuIcon("Icons.Compare"); + compareWith.Click += (_, _) => { - await repo.CompareBranchWithWorktreeAsync(branch); - e.Handled = true; + new ViewModels.CompareCommandPalette(repo, branch).Open(); }; - menu.Items.Add(compareWithWorktree); + + menu.Items.Add(pull); + menu.Items.Add(merge); + menu.Items.Add(rebase); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(interactiveRebase); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(compareWithHead); + menu.Items.Add(compareWith); } + 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.Icon = this.CreateMenuIcon("Icons.Edit"); editDescription.Click += async (_, e) => { var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description"); @@ -1132,7 +1277,8 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, var delete = new MenuItem(); delete.Header = App.Text("BranchCM.Delete", name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Icon = this.CreateMenuIcon("Icons.Clear"); + delete.Tag = "Delete/Back"; delete.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -1145,7 +1291,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, menu.Items.Add(new MenuItem() { Header = "-" }); var createBranch = new MenuItem(); - createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); + createBranch.Icon = this.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); createBranch.Click += (_, e) => { @@ -1155,7 +1301,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, }; var createTag = new MenuItem(); - createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); + createTag.Icon = this.CreateMenuIcon("Icons.Tag.Add"); createTag.Header = App.Text("CreateTag"); createTag.Click += (_, e) => { @@ -1172,7 +1318,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, { var createPR = new MenuItem(); createPR.Header = App.Text("BranchCM.CreatePR"); - createPR.Icon = App.CreateMenuIcon("Icons.CreatePR"); + createPR.Icon = this.CreateMenuIcon("Icons.CreatePR"); createPR.Click += (_, e) => { Native.OS.OpenBrowser(prURL); @@ -1185,7 +1331,7 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, menu.Items.Add(new MenuItem() { Header = "-" }); var archive = new MenuItem(); - archive.Icon = App.CreateMenuIcon("Icons.Archive"); + archive.Icon = this.CreateMenuIcon("Icons.Archive"); archive.Header = App.Text("Archive"); archive.Click += (_, e) => { @@ -1196,10 +1342,10 @@ public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, var copy = new MenuItem(); copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Icon = this.CreateMenuIcon("Icons.Copy"); copy.Click += async (_, e) => { - await App.CopyTextAsync(name); + await this.CopyTextAsync(name); e.Handled = true; }; @@ -1218,13 +1364,13 @@ private void TryToAddCustomActionsToBranchContextMenu(ViewModels.Repository repo var custom = new MenuItem(); custom.Header = App.Text("BranchCM.CustomAction"); - custom.Icon = App.CreateMenuIcon("Icons.Action"); + custom.Icon = this.CreateMenuIcon("Icons.Action"); foreach (var action in actions) { var (dup, label) = action; var item = new MenuItem(); - item.Icon = App.CreateMenuIcon("Icons.Action"); + item.Icon = this.CreateMenuIcon("Icons.Action"); item.Header = label; item.Click += async (_, e) => { @@ -1247,13 +1393,13 @@ private void TryToAddCustomActionsToRemoteContextMenu(ViewModels.Repository repo var custom = new MenuItem(); custom.Header = App.Text("RemoteCM.CustomAction"); - custom.Icon = App.CreateMenuIcon("Icons.Action"); + custom.Icon = this.CreateMenuIcon("Icons.Action"); foreach (var action in actions) { var (dup, label) = action; var item = new MenuItem(); - item.Icon = App.CreateMenuIcon("Icons.Action"); + item.Icon = this.CreateMenuIcon("Icons.Action"); item.Header = label; item.Click += async (_, e) => { @@ -1268,6 +1414,7 @@ private void TryToAddCustomActionsToRemoteContextMenu(ViewModels.Repository repo menu.Items.Add(new MenuItem() { Header = "-" }); } + private List _nodes = null; private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/CaptionButtons.axaml b/src/Views/CaptionButtons.axaml index b1ee6a7b2..948d87e9c 100644 --- a/src/Views/CaptionButtons.axaml +++ b/src/Views/CaptionButtons.axaml @@ -9,10 +9,10 @@ - - diff --git a/src/Views/CaptionButtons.axaml.cs b/src/Views/CaptionButtons.axaml.cs index 806d9cb5e..a22591673 100644 --- a/src/Views/CaptionButtons.axaml.cs +++ b/src/Views/CaptionButtons.axaml.cs @@ -1,7 +1,6 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.VisualTree; namespace SourceGit.Views { @@ -23,7 +22,7 @@ public CaptionButtons() private void MinimizeWindow(object _, RoutedEventArgs e) { - var window = this.FindAncestorOfType(); + var window = TopLevel.GetTopLevel(this) as Window; if (window != null) window.WindowState = WindowState.Minimized; @@ -32,7 +31,7 @@ private void MinimizeWindow(object _, RoutedEventArgs e) private void MaximizeOrRestoreWindow(object _, RoutedEventArgs e) { - var window = this.FindAncestorOfType(); + var window = TopLevel.GetTopLevel(this) as Window; if (window != null) window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; @@ -41,7 +40,7 @@ private void MaximizeOrRestoreWindow(object _, RoutedEventArgs e) private void CloseWindow(object _, RoutedEventArgs e) { - var window = this.FindAncestorOfType(); + var window = TopLevel.GetTopLevel(this) as Window; window?.Close(); e.Handled = true; diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml index a00570f50..be9143d1b 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -37,38 +37,43 @@ SelectionChanged="OnRowSelectionChanged"> - - + + + + - - - - - - - + + + + @@ -82,25 +87,28 @@ SelectionChanged="OnRowSelectionChanged"> - - + - - - - + + + - - + + @@ -114,21 +122,25 @@ SelectionChanged="OnRowSelectionChanged"> - - + - - - - - + + + + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 237a69da4..0a271664f 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -21,6 +21,10 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && DataContext is ViewModels.ChangeTreeNode { IsFolder: true } node) { + var container = this.FindAncestorOfType(); + if (container != null) + container.SelectedItem = node; + var tree = this.FindAncestorOfType(); tree?.ToggleNodeIsExpanded(node); } @@ -29,72 +33,125 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) } } - public class ChangeCollectionContainer : ListBox + public class ChangeCollectionContainer : ListBoxEx { protected override Type StyleKeyOverride => typeof(ListBox); protected override void OnKeyDown(KeyEventArgs e) { - if (SelectedItems is [ViewModels.ChangeTreeNode node]) + if (SelectedItems is [ViewModels.ChangeTreeNode node] && e.KeyModifiers == KeyModifiers.None) { - if (((e.Key == Key.Left && node.IsExpanded) || (e.Key == Key.Right && !node.IsExpanded)) && - e.KeyModifiers == KeyModifiers.None) + if (e.Key == Key.Left) { - this.FindAncestorOfType()?.ToggleNodeIsExpanded(node); - e.Handled = true; + if (node.IsExpanded && node.IsFolder) + { + this.FindAncestorOfType()?.ToggleNodeIsExpanded(node); + e.Handled = true; + } + else if (FindParent(node) is { } parent) + { + Select(parent); + e.Handled = true; + } + } + else if (e.Key == Key.Right && node.IsFolder) + { + if (!node.IsExpanded) + { + this.FindAncestorOfType()?.ToggleNodeIsExpanded(node); + e.Handled = true; + } + else if (node.Children.Count > 0) + { + Select(node.Children[0]); + e.Handled = true; + } } } - if (!e.Handled && e.Key != Key.Space && e.Key != Key.Enter) + if (!e.Handled) base.OnKeyDown(e); } + + private ViewModels.ChangeTreeNode FindParent(ViewModels.ChangeTreeNode item) + { + if (item.Depth == 0) + return null; + + var idx = Items.IndexOf(item); + if (idx < 1) + return null; + + for (var i = idx - 1; i >= 0; i--) + { + if (Items[i] is ViewModels.ChangeTreeNode node && node.Depth < item.Depth) + return node; + } + + return null; + } } public partial class ChangeCollectionView : UserControl { - public static readonly StyledProperty IsUnstagedChangeProperty = - AvaloniaProperty.Register(nameof(IsUnstagedChange)); + public static readonly DirectProperty IsUnstagedChangeProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsUnstagedChange), + static o => o.IsUnstagedChange, + static (o, v) => o.IsUnstagedChange = v); public bool IsUnstagedChange { - get => GetValue(IsUnstagedChangeProperty); - set => SetValue(IsUnstagedChangeProperty, value); + get => _isUnstagedChange; + set => SetAndRaise(IsUnstagedChangeProperty, ref _isUnstagedChange, value); } - public static readonly StyledProperty ViewModeProperty = - AvaloniaProperty.Register(nameof(ViewMode), Models.ChangeViewMode.Tree); + public static readonly DirectProperty ViewModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ViewMode), + static o => o.ViewMode, + static (o, v) => o.ViewMode = v); public Models.ChangeViewMode ViewMode { - get => GetValue(ViewModeProperty); - set => SetValue(ViewModeProperty, value); + get => _viewMode; + set => SetAndRaise(ViewModeProperty, ref _viewMode, value); } - public static readonly StyledProperty EnableCompactFoldersProperty = - AvaloniaProperty.Register(nameof(EnableCompactFolders)); + public static readonly DirectProperty EnableCompactFoldersProperty = + AvaloniaProperty.RegisterDirect( + nameof(EnableCompactFolders), + static o => o.EnableCompactFolders, + static (o, v) => o.EnableCompactFolders = v); public bool EnableCompactFolders { - get => GetValue(EnableCompactFoldersProperty); - set => SetValue(EnableCompactFoldersProperty, value); + get => _enableCompactFolders; + set => SetAndRaise(EnableCompactFoldersProperty, ref _enableCompactFolders, value); } - public static readonly StyledProperty> ChangesProperty = - AvaloniaProperty.Register>(nameof(Changes)); + public static readonly DirectProperty> ChangesProperty = + AvaloniaProperty.RegisterDirect>( + nameof(Changes), + static o => o.Changes, + static (o, v) => o.Changes = v); public List Changes { - get => GetValue(ChangesProperty); - set => SetValue(ChangesProperty, value); + get => _changes; + set => SetAndRaise(ChangesProperty, ref _changes, value); } - public static readonly StyledProperty> SelectedChangesProperty = - AvaloniaProperty.Register>(nameof(SelectedChanges)); + public static readonly DirectProperty> SelectedChangesProperty = + AvaloniaProperty.RegisterDirect>( + nameof(SelectedChanges), + static o => o.SelectedChanges, + static (o, v) => o.SelectedChanges = v); public List SelectedChanges { - get => GetValue(SelectedChangesProperty); - set => SetValue(SelectedChangesProperty, value); + get => _selectedChanges; + set => SetAndRaise(SelectedChangesProperty, ref _selectedChanges, value); } public static readonly RoutedEvent ChangeDoubleTappedEvent = @@ -147,7 +204,7 @@ public void ToggleNodeIsExpanded(ViewModels.ChangeTreeNode node) public Models.Change GetNextChangeWithoutSelection() { - var selected = SelectedChanges; + var selected = _selectedChanges; var changes = Changes; if (selected == null || selected.Count == 0) return changes.Count > 0 ? changes[0] : null; @@ -231,17 +288,17 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang private void OnRowDataContextChanged(object sender, EventArgs e) { - if (sender is not Control control) + if (sender is not Control { DataContext: { } ctx } control) return; - if (control.DataContext is ViewModels.ChangeTreeNode node) + if (ctx is ViewModels.ChangeTreeNode node) { if (node.Change is { } c) UpdateRowTips(control, c); else ToolTip.SetTip(control, node.FullPath); } - else if (control.DataContext is Models.Change change) + else if (ctx is Models.Change change) { UpdateRowTips(control, change); } @@ -253,8 +310,10 @@ private void OnRowDataContextChanged(object sender, EventArgs e) private void OnRowDoubleTapped(object sender, TappedEventArgs e) { - var grid = sender as Grid; - if (grid?.DataContext is ViewModels.ChangeTreeNode node) + if (sender is not Control { DataContext: { } ctx }) + return; + + if (ctx is ViewModels.ChangeTreeNode node) { if (node.IsFolder) { @@ -269,7 +328,7 @@ private void OnRowDoubleTapped(object sender, TappedEventArgs e) RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); } } - else if (grid?.DataContext is Models.Change) + else if (ctx is Models.Change) { RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); } @@ -297,7 +356,7 @@ private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) var old = SelectedChanges ?? []; if (old.Count != selected.Count) { - SetCurrentValue(SelectedChangesProperty, selected); + SelectedChanges = selected; } else { @@ -312,7 +371,7 @@ private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) } if (!allEquals) - SetCurrentValue(SelectedChangesProperty, selected); + SelectedChanges = selected; } _disableSelectionChangingEvent = false; @@ -335,7 +394,7 @@ private void UpdateDataSource(bool onlyViewModeChange) { _disableSelectionChangingEvent = !onlyViewModeChange; - var changes = Changes; + var changes = _changes; if (changes == null || changes.Count == 0) { Content = null; @@ -343,10 +402,10 @@ private void UpdateDataSource(bool onlyViewModeChange) return; } - var selected = SelectedChanges ?? []; + var selected = _selectedChanges ?? []; if (ViewMode == Models.ChangeViewMode.Tree) { - HashSet oldFolded = new HashSet(); + var oldFolded = new HashSet(); if (Content is ViewModels.ChangeCollectionAsTree oldTree) { foreach (var row in oldTree.Rows) @@ -407,7 +466,7 @@ private void UpdateSelection() _disableSelectionChangingEvent = true; - var selected = SelectedChanges ?? []; + var selected = _selectedChanges ?? []; if (Content is ViewModels.ChangeCollectionAsTree tree) { tree.SelectedRows.Clear(); @@ -415,7 +474,6 @@ private void UpdateSelection() if (selected.Count > 0) { var sets = new HashSet(selected); - var nodes = new List(); foreach (var row in tree.Rows) { @@ -470,6 +528,11 @@ private void UpdateRowTips(Control control, Models.Change change) ToolTip.SetTip(control, tip); } + private bool _isUnstagedChange = false; + private Models.ChangeViewMode _viewMode = Models.ChangeViewMode.Tree; + private bool _enableCompactFolders = false; + private List _changes = null; + private List _selectedChanges = null; private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index 4cbbf0a66..ee48cbc89 100644 --- a/src/Views/ChangeStatusIcon.cs +++ b/src/Views/ChangeStatusIcon.cs @@ -10,7 +10,7 @@ namespace SourceGit.Views { public class ChangeStatusIcon : Control { - private static readonly string[] INDICATOR = ["?", "±", "T", "+", "−", "➜", "❏", "★", "!"]; + private static readonly string[] INDICATOR = ["?", "M", "T", "+", "−", "➜", "❏", "?", "!"]; private static readonly Color[] COLOR = [ Colors.Transparent, @@ -20,36 +20,41 @@ public class ChangeStatusIcon : Control Colors.Tomato, Colors.Orchid, Colors.Goldenrod, - Colors.LimeGreen, + Colors.SkyBlue, Colors.OrangeRed, ]; - public static readonly StyledProperty IsUnstagedChangeProperty = - AvaloniaProperty.Register(nameof(IsUnstagedChange)); + public static readonly DirectProperty IsUnstagedChangeProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsUnstagedChange), + static o => o.IsUnstagedChange, + static (o, v) => o.IsUnstagedChange = v); public bool IsUnstagedChange { - get => GetValue(IsUnstagedChangeProperty); - set => SetValue(IsUnstagedChangeProperty, value); + get => _isUnstagedChange; + set => SetAndRaise(IsUnstagedChangeProperty, ref _isUnstagedChange, value); } - public static readonly StyledProperty ChangeProperty = - AvaloniaProperty.Register(nameof(Change)); + public static readonly DirectProperty ChangeProperty = + AvaloniaProperty.RegisterDirect( + nameof(Change), + static o => o.Change, + static (o, v) => o.Change = v); public Models.Change Change { - get => GetValue(ChangeProperty); - set => SetValue(ChangeProperty, value); + get => _change; + set => SetAndRaise(ChangeProperty, ref _change, value); } public override void Render(DrawingContext context) { - if (Change == null || Bounds.Width <= 0) + if (_change == null || Bounds.Width <= 0) return; - var typeface = new Typeface("fonts:SourceGit#JetBrains Mono"); - - var idx = (int)(IsUnstagedChange ? Change.WorkTree : Change.Index); + var typeface = new Typeface("fonts:SourceGit#JetBrains Mono NL"); + var idx = (int)(_isUnstagedChange ? _change.WorkTree : _change.Index); var indicator = INDICATOR[idx]; var color = COLOR[idx]; var hsl = color.ToHsl(); @@ -82,10 +87,13 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { base.OnPropertyChanged(change); - if (change.Property == IsUnstagedChangeProperty || - change.Property == ChangeProperty || - (change.Property.Name == "ActualThemeVariant" && change.NewValue != null)) + if (change.Property == IsUnstagedChangeProperty || change.Property == ChangeProperty) + InvalidateVisual(); + else if (change.Property.Name == nameof(ActualThemeVariant) && change.NewValue != null) InvalidateVisual(); } + + private bool _isUnstagedChange = false; + private Models.Change _change = null; } } diff --git a/src/Views/ChangeSubmoduleUrl.axaml b/src/Views/ChangeSubmoduleUrl.axaml index a12a04890..bf3d7b3db 100644 --- a/src/Views/ChangeSubmoduleUrl.axaml +++ b/src/Views/ChangeSubmoduleUrl.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.ChangeSubmoduleUrl" x:DataType="vm:ChangeSubmoduleUrl"> @@ -16,7 +17,7 @@ Classes="bold" Text="{DynamicResource Text.ChangeSubmoduleUrl}"/> - + + Text="{Binding Url, Mode=TwoWay}"> + + + + + + + + diff --git a/src/Views/ChangeViewModeSwitcher.axaml.cs b/src/Views/ChangeViewModeSwitcher.axaml.cs index ed3066196..a51eccefd 100644 --- a/src/Views/ChangeViewModeSwitcher.axaml.cs +++ b/src/Views/ChangeViewModeSwitcher.axaml.cs @@ -6,13 +6,16 @@ namespace SourceGit.Views { public partial class ChangeViewModeSwitcher : UserControl { - public static readonly StyledProperty ViewModeProperty = - AvaloniaProperty.Register(nameof(ViewMode)); + public static readonly DirectProperty ViewModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ViewMode), + static o => o.ViewMode, + static (o, v) => o.ViewMode = v); public Models.ChangeViewMode ViewMode { - get => GetValue(ViewModeProperty); - set => SetValue(ViewModeProperty, value); + get => _viewMode; + set => SetAndRaise(ViewModeProperty, ref _viewMode, value); } public ChangeViewModeSwitcher() @@ -37,5 +40,7 @@ private void SwitchToTree(object sender, RoutedEventArgs e) ViewMode = Models.ChangeViewMode.Tree; e.Handled = true; } + + private Models.ChangeViewMode _viewMode = Models.ChangeViewMode.List; } } diff --git a/src/Views/Chart.cs b/src/Views/Chart.cs new file mode 100644 index 000000000..6eafbd44d --- /dev/null +++ b/src/Views/Chart.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public record ChartToolTip(string Title, bool HasUser, int All, int User); + + public class Chart : Control + { + public static readonly DirectProperty SamplesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Samples), + static o => o.Samples, + static (o, v) => o.Samples = v); + + public Models.StatisticsSamples Samples + { + get => _samples; + set => SetAndRaise(SamplesProperty, ref _samples, value); + } + + public static readonly StyledProperty LabelFontFamilyProperty = + AvaloniaProperty.Register(nameof(LabelFontFamily)); + + public FontFamily LabelFontFamily + { + get => GetValue(LabelFontFamilyProperty); + set => SetValue(LabelFontFamilyProperty, value); + } + + public static readonly StyledProperty SampleBrushProperty = + AvaloniaProperty.Register(nameof(SampleBrush), Brushes.SkyBlue); + + public IBrush SampleBrush + { + get => GetValue(SampleBrushProperty); + set => SetValue(SampleBrushProperty, value); + } + + public Chart() + { + ToolTip.SetPlacement(this, PlacementMode.Pointer); + ToolTip.SetShowDelay(this, 0); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var samples = _samples; + if (samples == null || samples.Count == 0) + return; + + var w = Bounds.Width; + var h = Bounds.Height; + context.FillRectangle(Brushes.Transparent, new Rect(0, 0, w, h)); + + var count = samples.Count; + var maxValue = samples.MaxValue; + var time = samples.EndTime; + var minTime = samples.StartTime; + var hasUserSamples = samples.HasSpecialUser; + + var labelPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), .5); + var labelTypeface = new Typeface(LabelFontFamily); + var corner = new CornerRadius(2, 2, 0, 0); + var sampleBrush = SampleBrush; + + var leftMargin = 0.0; + for (var i = 1; i <= 8; i++) + { + var percent = i * 0.125; + var value = Math.Floor(maxValue * (1.0 - percent)); + var y = Math.Floor((h - 24) * percent) + 0.5; + + var label = new FormattedText( + $"{value}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + labelTypeface, + 11, + Brushes.Gray); + + if (leftMargin == 0) + leftMargin = label.Width + 24.0; + + context.DrawText(label, new Point(leftMargin - label.Width - 8.0, y - label.Height * 0.5)); + context.DrawLine(labelPen, new Point(leftMargin, y), new Point(w, y)); + } + + var step = Math.Max((w - leftMargin) / count, 10.0); + _maxOffsetX = Math.Max(count * step - (w - leftMargin), 0); + _hitBoxes.Clear(); + _lastHitted = new Rect(0, 0, 0, 0); + + var x = w + Math.Min(_maxOffsetX, _offsetX); + var sampleW = hasUserSamples ? Math.Min(step * 0.5 - 2.5, 14.0) : Math.Min(step - 3, 28.0); + var maxSampleH = h - 24.0; + var maxLabelEndX = w - 4.0; + + using var clip = context.PushClip(new Rect(leftMargin, 0, w - leftMargin, h)); + do + { + var (label, total, user) = samples.GetSample(time); + + if (x - step <= w && total > 0) + { + if (hasUserSamples) + { + var startX = x - step * 0.5 - 1 - sampleW; + var startY = maxSampleH * (1.0 - total * 1.0 / maxValue); + var rect = new Rect(startX, startY, sampleW, maxSampleH - startY); + + using (context.PushOpacity(0.2)) + { + context.DrawRectangle(sampleBrush, null, new RoundedRect(rect, corner)); + } + + if (user > 0) + { + var userStartX = startX + sampleW + 2; + var userStartY = maxSampleH * (1.0 - user * 1.0 / maxValue); + var userRect = new Rect(userStartX, userStartY, sampleW, maxSampleH - userStartY); + context.DrawRectangle(sampleBrush, null, new RoundedRect(userRect, corner)); + } + + var hitRect = new Rect(startX, startY, sampleW * 2 + 2, maxSampleH - startY); + _hitBoxes.Add(new(hitRect, new(label, true, total, user))); + } + else + { + var startX = x - step * 0.5 - sampleW * 0.5; + var startY = maxSampleH * (1.0 - total * 1.0 / maxValue); + var rect = new Rect(startX, startY, sampleW, maxSampleH - startY); + + context.DrawRectangle(sampleBrush, null, new RoundedRect(rect, corner)); + _hitBoxes.Add(new(rect, new(label, false, total, 0))); + } + } + + if (x <= w) + { + var formattedLabel = new FormattedText( + label, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + labelTypeface, + 11, + Brushes.Gray); + + var labelCenterX = x - step * 0.5; + var labelEndX = labelCenterX + formattedLabel.Width * 0.5; + if (labelEndX <= maxLabelEndX) + { + var labelStartX = labelCenterX - formattedLabel.Width * 0.5; + var labelStartY = h - formattedLabel.Height - 2.0; + context.DrawText(formattedLabel, new Point(labelStartX, labelStartY)); + maxLabelEndX = labelStartX - 16.0; + } + } + + if (x <= leftMargin) + break; + + x -= step; + + time = samples.NextSampleTime(time); + if (time < minTime) + break; + } while (true); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SamplesProperty) + { + _offsetX = 0; + InvalidateVisual(); + } + else if (change.Property == SampleBrushProperty) + { + InvalidateVisual(); + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + _isDraging = true; + _lastDragX = e.GetPosition(this).X; + e.Pointer.Capture(this); + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + if (_isDraging) + { + var posX = e.GetPosition(this).X; + if (Math.Abs(posX - _lastDragX) < 0.5f) + return; + + var desired = Math.Max(0, Math.Min(_offsetX + posX - _lastDragX, _maxOffsetX)); + if (Math.Abs(desired - _offsetX) < 0.5f) + return; + + _offsetX = desired; + _lastDragX = posX; + _lastHitted = new Rect(0, 0, 0, 0); + InvalidateVisual(); + } + else + { + var p = e.GetPosition(this); + + if (!_lastHitted.Contains(p)) + { + foreach (var box in _hitBoxes) + { + if (box.Rect.Contains(p)) + { + ToolTip.SetTip(this, box.ToolTip); + _lastHitted = box.Rect; + return; + } + } + + _lastHitted = new Rect(0, 0, 0, 0); + ToolTip.SetTip(this, null); + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + _isDraging = false; + e.Pointer.Capture(null); + } + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + e.Handled = true; + + var deltaX = e.KeyModifiers == KeyModifiers.Shift ? e.Delta.Y : e.Delta.X; + if (deltaX == 0) + return; + + deltaX = Math.Min(32, Math.Max(-32, _maxOffsetX * deltaX)); + + var desired = Math.Max(0, Math.Min(_offsetX + deltaX, _maxOffsetX)); + if (Math.Abs(desired - _offsetX) < 0.1) + return; + + _offsetX = desired; + _isDraging = false; + _lastHitted = new Rect(0, 0, 0, 0); + InvalidateVisual(); + } + + private record HitBox(Rect Rect, ChartToolTip ToolTip); + + private Models.StatisticsSamples _samples = null; + private double _offsetX = 0; + private double _maxOffsetX = 0; + private bool _isDraging = false; + private double _lastDragX = 0; + private List _hitBoxes = []; + private Rect _lastHitted = new(0, 0, 0, 0); + } +} diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml index 0c8fca5bc..3d91c7072 100644 --- a/src/Views/Checkout.axaml +++ b/src/Views/Checkout.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Checkout" x:DataType="vm:Checkout"> @@ -18,41 +19,27 @@ Text="{DynamicResource Text.Checkout}"/> - - - - - - - + - + - - - - - - - + + + + diff --git a/src/Views/CheckoutAndFastForward.axaml b/src/Views/CheckoutAndFastForward.axaml index 04edff135..42fbf544c 100644 --- a/src/Views/CheckoutAndFastForward.axaml +++ b/src/Views/CheckoutAndFastForward.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CheckoutAndFastForward" x:DataType="vm:CheckoutAndFastForward"> @@ -18,14 +19,7 @@ Text="{DynamicResource Text.Checkout.WithFastForward}"/> - - - - - - - - + - - - - - - - + + + + diff --git a/src/Views/CheckoutBranchFromStash.axaml b/src/Views/CheckoutBranchFromStash.axaml new file mode 100644 index 000000000..037410af5 --- /dev/null +++ b/src/Views/CheckoutBranchFromStash.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutBranchFromStash.axaml.cs b/src/Views/CheckoutBranchFromStash.axaml.cs new file mode 100644 index 000000000..b05e4fceb --- /dev/null +++ b/src/Views/CheckoutBranchFromStash.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class CheckoutBranchFromStash : UserControl + { + public CheckoutBranchFromStash() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/CheckoutCommandPalette.axaml b/src/Views/CheckoutCommandPalette.axaml index 17897a26b..ad881f821 100644 --- a/src/Views/CheckoutCommandPalette.axaml +++ b/src/Views/CheckoutCommandPalette.axaml @@ -9,7 +9,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CheckoutCommandPalette" x:DataType="vm:CheckoutCommandPalette"> - + - + + + + + - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Compare.axaml.cs b/src/Views/Compare.axaml.cs new file mode 100644 index 000000000..ad5d69312 --- /dev/null +++ b/src/Views/Compare.axaml.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views +{ + public partial class Compare : ChromelessWindow + { + public Compare() + { + InitializeComponent(); + } + + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) + { + if (DataContext is ViewModels.Compare { SelectedChanges: { Count: > 0 } selected } vm && + sender is ChangeCollectionView view) + { + var menu = new ContextMenu(); + + var patch = new MenuItem(); + patch.Header = App.Text("FileCM.SaveAsPatch"); + patch.Icon = this.CreateMenuIcon("Icons.Save"); + 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; + var succ = await vm.SaveChangesAsPatchAsync(selected, saveTo); + if (succ) + await new Alert().ShowAsync(this, "Save patch successfully.", false); + } + } + catch (Exception exception) + { + await new Alert().ShowAsync(this, $"Failed to save as patch: {exception.Message}", true); + } + + e.Handled = true; + }; + + if (selected.Count == 1) + { + var change = selected[0]; + var openWithMerger = new MenuItem(); + openWithMerger.Header = App.Text("OpenInExternalMergeTool"); + openWithMerger.Icon = this.CreateMenuIcon("Icons.OpenWith"); + openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; + openWithMerger.Click += (_, ev) => + { + vm.OpenInExternalDiffTool(change); + ev.Handled = true; + }; + menu.Items.Add(openWithMerger); + + if (change.Index != Models.ChangeState.Deleted) + { + var full = vm.GetAbsPath(change.Path); + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = File.Exists(full); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(full); + ev.Handled = true; + }; + menu.Items.Add(explore); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(patch); + + if (vm.CanResetFiles) + { + var resetToLeft = new MenuItem(); + resetToLeft.Header = App.Text("ChangeCM.ResetFileTo", vm.BaseName); + resetToLeft.Icon = this.CreateMenuIcon("Icons.File.Checkout"); + resetToLeft.Click += async (_, ev) => + { + await vm.ResetToLeftAsync(change); + ev.Handled = true; + }; + + var resetToRight = new MenuItem(); + resetToRight.Header = App.Text("ChangeCM.ResetFileTo", vm.ToName); + resetToRight.Icon = this.CreateMenuIcon("Icons.File.Checkout"); + resetToRight.Click += async (_, ev) => + { + await vm.ResetToRightAsync(change); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(resetToLeft); + menu.Items.Add(resetToRight); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = this.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + await this.CopyTextAsync(change.Path); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = this.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + await this.CopyTextAsync(vm.GetAbsPath(change.Path)); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + else + { + menu.Items.Add(patch); + + if (vm.CanResetFiles) + { + var resetToLeft = new MenuItem(); + resetToLeft.Header = App.Text("ChangeCM.ResetFileTo", vm.BaseName); + resetToLeft.Icon = this.CreateMenuIcon("Icons.File.Checkout"); + resetToLeft.Click += async (_, ev) => + { + await vm.ResetMultipleToLeftAsync(selected); + ev.Handled = true; + }; + + var resetToRight = new MenuItem(); + resetToRight.Header = App.Text("ChangeCM.ResetFileTo", vm.ToName); + resetToRight.Icon = this.CreateMenuIcon("Icons.File.Checkout"); + resetToRight.Click += async (_, ev) => + { + await vm.ResetMultipleToRightAsync(selected); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(resetToLeft); + menu.Items.Add(resetToRight); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = this.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 this.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = this.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(vm.GetAbsPath(c.Path)); + + await this.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + + menu.Open(view); + } + + e.Handled = true; + } + + private void OnCommitListContextRequested(object sender, ContextRequestedEventArgs e) + { + if (DataContext is not ViewModels.Compare vm) + return; + + if (sender is ListBox { SelectedItems: { Count: > 0 } selected } listBox) + { + var commits = new List(); + foreach (var o in selected) + { + if (o is Models.Commit c) + commits.Add(c); + } + + if (commits.Count == 0) + return; + + commits.Sort((l, r) => l.CommitterTime.CompareTo(r.CommitterTime)); + + var menu = new ContextMenu(); + var hasCurrentHead = vm.BaseHead.IsCurrentHead || vm.ToHead.IsCurrentHead; + if (hasCurrentHead && listBox.Tag is Models.Commit { IsCurrentHead: false }) + { + var cherryPick = new MenuItem(); + cherryPick.Header = App.Text("CommitCM.CherryPickMultiple"); + cherryPick.Icon = this.CreateMenuIcon("Icons.CherryPick"); + cherryPick.Click += (_, ev) => + { + vm.CherryPick(commits); + ev.Handled = true; + }; + + menu.Items.Add(cherryPick); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = this.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in commits) + builder.Append(c.SHA.Substring(0, 10)).Append(" - ").AppendLine(c.Subject); + + await this.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + menu.Items.Add(copy); + menu.Open(listBox); + e.Handled = true; + } + } + + private void OnCommitListSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (DataContext is ViewModels.Compare vm && + sender is ListBox { SelectedItems: { Count: 1 } selected } listBox && + selected[0] is Models.Commit c) + { + vm.NavigateTo(c.SHA); + e.Handled = true; + } + } + + private void OnPressedSHA(object sender, PointerPressedEventArgs e) + { + if (DataContext is ViewModels.Compare vm && sender is TextBlock block) + vm.NavigateTo(block.Text); + + e.Handled = true; + } + + private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) + { + if (DataContext is not ViewModels.Compare vm) + return; + + if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) + return; + + var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + if (e.Key == Key.C && e.KeyModifiers.HasFlag(cmdKey)) + { + 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 this.CopyTextAsync(builder.ToString()); + e.Handled = true; + } + else if (e.Key == Key.F && e.KeyModifiers == cmdKey) + { + ChangeSearchBox.Focus(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/CompareCommandPalette.axaml b/src/Views/CompareCommandPalette.axaml new file mode 100644 index 000000000..e8b32115c --- /dev/null +++ b/src/Views/CompareCommandPalette.axaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchCompareCommandPalette.axaml.cs b/src/Views/CompareCommandPalette.axaml.cs similarity index 67% rename from src/Views/BranchCompareCommandPalette.axaml.cs rename to src/Views/CompareCommandPalette.axaml.cs index 936ae94c8..44809f278 100644 --- a/src/Views/BranchCompareCommandPalette.axaml.cs +++ b/src/Views/CompareCommandPalette.axaml.cs @@ -3,9 +3,9 @@ namespace SourceGit.Views { - public partial class BranchCompareCommandPalette : UserControl + public partial class CompareCommandPalette : UserControl { - public BranchCompareCommandPalette() + public CompareCommandPalette() { InitializeComponent(); } @@ -14,17 +14,17 @@ protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); - if (DataContext is not ViewModels.BranchCompareCommandPalette vm) + if (DataContext is not ViewModels.CompareCommandPalette vm) return; if (e.Key == Key.Enter) { - vm.Launch(); + this.ShowWindow(vm.Launch()); e.Handled = true; } else if (e.Key == Key.Up) { - if (BranchListBox.IsKeyboardFocusWithin) + if (RefsListBox.IsKeyboardFocusWithin) { FilterTextBox.Focus(NavigationMethod.Directional); e.Handled = true; @@ -35,14 +35,14 @@ protected override void OnKeyDown(KeyEventArgs e) { if (FilterTextBox.IsKeyboardFocusWithin) { - if (vm.Branches.Count > 0) - BranchListBox.Focus(NavigationMethod.Directional); + if (vm.Refs.Count > 0) + RefsListBox.Focus(NavigationMethod.Directional); e.Handled = true; return; } - if (BranchListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + if (RefsListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) { FilterTextBox.Focus(NavigationMethod.Directional); e.Handled = true; @@ -53,9 +53,9 @@ protected override void OnKeyDown(KeyEventArgs e) private void OnItemTapped(object sender, TappedEventArgs e) { - if (DataContext is ViewModels.BranchCompareCommandPalette vm) + if (DataContext is ViewModels.CompareCommandPalette vm) { - vm.Launch(); + this.ShowWindow(vm.Launch()); e.Handled = true; } } diff --git a/src/Views/ConfigureCustomActionControls.axaml b/src/Views/ConfigureCustomActionControls.axaml index e24bc0101..520afeff2 100644 --- a/src/Views/ConfigureCustomActionControls.axaml +++ b/src/Views/ConfigureCustomActionControls.axaml @@ -129,9 +129,11 @@ - + + + @@ -160,7 +162,14 @@ + Text="{Binding StringValue, Mode=TwoWay}"> + + + + + + + + + + + + + + - + - + - + diff --git a/src/Views/Conflict.axaml.cs b/src/Views/Conflict.axaml.cs index a913d860e..0ed1fa8f1 100644 --- a/src/Views/Conflict.axaml.cs +++ b/src/Views/Conflict.axaml.cs @@ -37,10 +37,21 @@ private async void OnUseMine(object _, RoutedEventArgs e) e.Handled = true; } - private async void OnOpenExternalMergeTool(object _, RoutedEventArgs e) + private void OnMerge(object _, RoutedEventArgs e) { if (DataContext is ViewModels.Conflict vm) - await vm.OpenExternalMergeToolAsync(); + { + var request = vm.CreateOpenMergeEditorRequest(); + this.ShowWindow(request); + } + + e.Handled = true; + } + + private async void OnMergeExternal(object _, RoutedEventArgs e) + { + if (DataContext is ViewModels.Conflict vm) + await vm.MergeExternalAsync(); e.Handled = true; } diff --git a/src/Views/ControlExtensions.cs b/src/Views/ControlExtensions.cs new file mode 100644 index 000000000..33c20b4f2 --- /dev/null +++ b/src/Views/ControlExtensions.cs @@ -0,0 +1,108 @@ +using System; +using System.Threading.Tasks; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public static class ControlExtensions + { + public static Control CreateFromViewModels(object data) + { + var dataTypeName = data.GetType().FullName; + if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal)) + return null; + + var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views."); + var viewType = Type.GetType(viewTypeName); + if (viewType != null) + return Activator.CreateInstance(viewType) as Control; + + return null; + } + + public static async Task CopyTextAsync(this Control control, string text) + { + var clipboard = TopLevel.GetTopLevel(control)?.Clipboard; + if (clipboard != null) + await clipboard.SetTextAsync(text); + } + + public static Path CreateMenuIcon(this Control control, string iconKey) + { + if (control?.FindResource(iconKey) is StreamGeometry geo) + { + return new Path() + { + Data = geo, + Width = 12, + Height = 12, + Stretch = Stretch.Uniform + }; + } + + return null; + } + + public static void ShowWindow(this Control control, object data) + { + if (data == null) + return; + + if (data is not ChromelessWindow window) + { + window = CreateFromViewModels(data) as ChromelessWindow; + if (window == null) + return; + + window.DataContext = data; + } + + do + { + var owner = TopLevel.GetTopLevel(control) as Window; + if (owner != null) + { + // Get the screen where current window locates. + var screen = owner.Screens.ScreenFromWindow(owner) ?? owner.Screens.Primary; + if (screen == null) + break; + + // Calculate the startup position (Center Screen Mode) of target window + var rect = new PixelRect(PixelSize.FromSize(window.ClientSize, owner.DesktopScaling)); + var centeredRect = screen.WorkingArea.CenterRect(rect); + if (owner.Screens.ScreenFromPoint(centeredRect.Position) == null) + break; + + // Use the startup position + window.WindowStartupLocation = WindowStartupLocation.Manual; + window.Position = centeredRect.Position; + } + } while (false); + + window.Show(); + } + + public static Task ShowDialogAsync(this Control control, object data) + { + var owner = TopLevel.GetTopLevel(control) as Window; + if (owner == null) + return null; + + if (data is ChromelessWindow window) + return window.ShowDialog(owner); + + window = CreateFromViewModels(data) as ChromelessWindow; + if (window != null) + { + window.DataContext = data; + return window.ShowDialog(owner); + } + + return null; + } + } +} diff --git a/src/Views/ConventionalCommitMessageBuilder.axaml b/src/Views/ConventionalCommitMessageBuilder.axaml index f0e1a44d1..394901dc9 100644 --- a/src/Views/ConventionalCommitMessageBuilder.axaml +++ b/src/Views/ConventionalCommitMessageBuilder.axaml @@ -87,7 +87,7 @@ @@ -98,7 +98,7 @@ @@ -131,7 +131,7 @@ ScrollViewer.VerticalScrollBarVisibility="Auto" VerticalAlignment="Center" VerticalContentAlignment="Top" - CornerRadius="2" + CornerRadius="3" Watermark="{DynamicResource Text.Optional}" Text="{Binding BreakingChanges, Mode=TwoWay}"/> @@ -142,7 +142,7 @@ diff --git a/src/Views/CreateBranch.axaml b/src/Views/CreateBranch.axaml index b53a23a2c..670a6ff9a 100644 --- a/src/Views/CreateBranch.axaml +++ b/src/Views/CreateBranch.axaml @@ -19,13 +19,13 @@ Classes="bold" Text="{DynamicResource Text.CreateBranch.Title}"/> - - + + - + - + - - + + @@ -63,49 +72,42 @@ + Watermark="{DynamicResource Text.CreateBranch.Name.Placeholder}"/> - - - - - - + + + + + + + + + + + + + + + + + ToolTip.Tip="{Binding OverrideTip, Mode=OneWay}"/> - - - - - - - - - - diff --git a/src/Views/CreateGroup.axaml b/src/Views/CreateGroup.axaml index 15b570203..b939224e4 100644 --- a/src/Views/CreateGroup.axaml +++ b/src/Views/CreateGroup.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" - xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CreateGroup" x:DataType="vm:CreateGroup"> @@ -20,7 +19,7 @@ - + diff --git a/src/Views/CreateTag.axaml b/src/Views/CreateTag.axaml index 07f74589d..eefb54f6b 100644 --- a/src/Views/CreateTag.axaml +++ b/src/Views/CreateTag.axaml @@ -19,7 +19,7 @@ Classes="bold" Text="{DynamicResource Text.CreateTag.Title}"/> - + - - + + @@ -51,10 +60,9 @@ + Watermark="{DynamicResource Text.CreateTag.Name.Placeholder}"/> @@ -78,7 +85,7 @@ Height="64" AcceptsReturn="True" AcceptsTab="False" VerticalAlignment="Center" VerticalContentAlignment="Top" - CornerRadius="2" + CornerRadius="3" Watermark="{DynamicResource Text.CreateTag.Message.Placeholder}" Text="{Binding Message, Mode=TwoWay}" IsVisible="{Binding Annotated}" diff --git a/src/Views/DateTimePresenter.cs b/src/Views/DateTimePresenter.cs new file mode 100644 index 000000000..eddfec96e --- /dev/null +++ b/src/Views/DateTimePresenter.cs @@ -0,0 +1,97 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; + +namespace SourceGit.Views +{ + public class DateTimePresenter : TextBlock + { + public static readonly DirectProperty ShowDateOnlyProperty = + AvaloniaProperty.RegisterDirect( + nameof(ShowDateOnly), + static o => o.ShowDateOnly, + static (o, v) => o.ShowDateOnly = v); + + public bool ShowDateOnly + { + get => _showDateOnly; + set => SetAndRaise(ShowDateOnlyProperty, ref _showDateOnly, value); + } + + public static readonly DirectProperty Use24HoursProperty = + AvaloniaProperty.RegisterDirect( + nameof(Use24Hours), + static o => o.Use24Hours, + static (o, v) => o.Use24Hours = v); + + public bool Use24Hours + { + get => _use24Hours; + set => SetAndRaise(Use24HoursProperty, ref _use24Hours, value); + } + + public static readonly DirectProperty DateTimeFormatProperty = + AvaloniaProperty.RegisterDirect( + nameof(DateTimeFormat), + static o => o.DateTimeFormat, + static (o, v) => o.DateTimeFormat = v); + + public int DateTimeFormat + { + get => _dateTimeFormat; + set => SetAndRaise(DateTimeFormatProperty, ref _dateTimeFormat, value); + } + + public static readonly DirectProperty TimestampProperty = + AvaloniaProperty.RegisterDirect( + nameof(Timestamp), + static o => o.Timestamp, + static (o, v) => o.Timestamp = v); + + public ulong Timestamp + { + get => _timestamp; + set => SetAndRaise(TimestampProperty, ref _timestamp, value); + } + + protected override Type StyleKeyOverride => typeof(TextBlock); + + public DateTimePresenter() + { + Bind(Use24HoursProperty, new Binding() + { + Mode = BindingMode.OneWay, + Source = ViewModels.Preferences.Instance, + Path = "Use24Hours" + }); + + Bind(DateTimeFormatProperty, new Binding() + { + Mode = BindingMode.OneWay, + Source = ViewModels.Preferences.Instance, + Path = "DateTimeFormat" + }); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ShowDateOnlyProperty || + change.Property == Use24HoursProperty || + change.Property == DateTimeFormatProperty || + change.Property == TimestampProperty) + { + var text = Models.DateTimeFormat.Format(Timestamp, ShowDateOnly); + SetCurrentValue(TextProperty, text); + } + } + + private bool _showDateOnly = false; + private bool _use24Hours = true; + private int _dateTimeFormat = 0; + private ulong _timestamp = 0; + } +} diff --git a/src/Views/DealWithLocalChangesMethod.axaml b/src/Views/DealWithLocalChangesMethod.axaml new file mode 100644 index 000000000..c0663173b --- /dev/null +++ b/src/Views/DealWithLocalChangesMethod.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + diff --git a/src/Views/DealWithLocalChangesMethod.axaml.cs b/src/Views/DealWithLocalChangesMethod.axaml.cs new file mode 100644 index 000000000..a55510308 --- /dev/null +++ b/src/Views/DealWithLocalChangesMethod.axaml.cs @@ -0,0 +1,91 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class DealWithLocalChangesMethod : UserControl + { + public static readonly DirectProperty MethodProperty = + AvaloniaProperty.RegisterDirect( + nameof(Method), + static o => o.Method, + static (o, v) => o.Method = v); + + public Models.DealWithLocalChanges Method + { + get => _method; + set => SetAndRaise(MethodProperty, ref _method, value); + } + + public DealWithLocalChangesMethod() + { + Focusable = true; + InitializeComponent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MethodProperty) + UpdateRadioButtons(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Up) + { + if (Method == Models.DealWithLocalChanges.StashAndReapply) + Method = Models.DealWithLocalChanges.DoNothing; + else if (Method == Models.DealWithLocalChanges.Discard) + Method = Models.DealWithLocalChanges.StashAndReapply; + e.Handled = true; + } + else if (e.Key == Key.Down) + { + if (Method == Models.DealWithLocalChanges.DoNothing) + Method = Models.DealWithLocalChanges.StashAndReapply; + else if (Method == Models.DealWithLocalChanges.StashAndReapply) + Method = Models.DealWithLocalChanges.Discard; + e.Handled = true; + } + } + + private void OnRadioButtonClicked(object sender, RoutedEventArgs e) + { + if (sender is RadioButton { Tag: Models.DealWithLocalChanges way }) + { + Method = way; + e.Handled = true; + } + } + + private void UpdateRadioButtons() + { + switch (_method) + { + case Models.DealWithLocalChanges.DoNothing: + RadioDoNothing.IsChecked = true; + RadioStashAndReapply.IsChecked = false; + RadioDiscard.IsChecked = false; + break; + case Models.DealWithLocalChanges.StashAndReapply: + RadioDoNothing.IsChecked = false; + RadioStashAndReapply.IsChecked = true; + RadioDiscard.IsChecked = false; + break; + default: + RadioDoNothing.IsChecked = false; + RadioStashAndReapply.IsChecked = false; + RadioDiscard.IsChecked = true; + break; + } + } + + private Models.DealWithLocalChanges _method = Models.DealWithLocalChanges.DoNothing; + } +} diff --git a/src/Views/DeleteBranch.axaml b/src/Views/DeleteBranch.axaml index a2640799a..3cd83e73d 100644 --- a/src/Views/DeleteBranch.axaml +++ b/src/Views/DeleteBranch.axaml @@ -37,19 +37,24 @@ - - - + + - - - - - + + + + + diff --git a/src/Views/DeleteMultipleBranches.axaml b/src/Views/DeleteMultipleBranches.axaml index 6bf42a340..10d026f7c 100644 --- a/src/Views/DeleteMultipleBranches.axaml +++ b/src/Views/DeleteMultipleBranches.axaml @@ -18,7 +18,13 @@ Text="{DynamicResource Text.DeleteMultiBranch}"/> - + + + + + + + + @@ -70,10 +82,9 @@ - + diff --git a/src/Views/DeleteRemote.axaml b/src/Views/DeleteRemote.axaml index ea5c39b80..f8ab1033d 100644 --- a/src/Views/DeleteRemote.axaml +++ b/src/Views/DeleteRemote.axaml @@ -16,7 +16,7 @@ Classes="bold" Text="{DynamicResource Text.DeleteRemote}"/> - + diff --git a/src/Views/DeleteRepositoryNode.axaml b/src/Views/DeleteRepositoryNode.axaml index a1b6f6540..5971e250d 100644 --- a/src/Views/DeleteRepositoryNode.axaml +++ b/src/Views/DeleteRepositoryNode.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:c="using:SourceGit.Converters" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DeleteRepositoryNode" x:DataType="vm:DeleteRepositoryNode"> @@ -20,7 +21,7 @@ Text="{DynamicResource Text.DeleteRepositoryNode.TitleForRepository}" IsVisible="{Binding Node.IsRepository}"/> - + - + - + - + - + - - - + + + + + + + + + + + + + - + - - - - @@ -119,7 +139,7 @@ - + @@ -127,9 +147,10 @@ + ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.All}" + PropertyChanged="OnToggleButtonPropertyChanged"> @@ -149,20 +170,13 @@ - + - - - - + ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}" + PropertyChanged="OnToggleButtonPropertyChanged"> - - + diff --git a/src/Views/EditRemote.axaml.cs b/src/Views/EditRemote.axaml.cs index 7d88704e5..655cbdcd2 100644 --- a/src/Views/EditRemote.axaml.cs +++ b/src/Views/EditRemote.axaml.cs @@ -17,7 +17,12 @@ private async void SelectSSHKey(object _, RoutedEventArgs e) if (toplevel == null) return; - var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; + var options = new FilePickerOpenOptions() + { + AllowMultiple = false, + FileTypeFilter = [new("SSHKey") { Patterns = ["*"] }] + }; + var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options); if (selected.Count == 1) TxtSshKey.Text = selected[0].Path.LocalPath; diff --git a/src/Views/EditRepositoryNode.axaml b/src/Views/EditRepositoryNode.axaml index 4b42434dd..8b9d66340 100644 --- a/src/Views/EditRepositoryNode.axaml +++ b/src/Views/EditRepositoryNode.axaml @@ -2,7 +2,6 @@ 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:c="using:SourceGit.Converters" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" @@ -26,33 +25,42 @@ IsVisible="{Binding IsRepository}"/> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/Views/ExecuteCustomAction.axaml b/src/Views/ExecuteCustomAction.axaml index b0525c123..6769f9481 100644 --- a/src/Views/ExecuteCustomAction.axaml +++ b/src/Views/ExecuteCustomAction.axaml @@ -4,11 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.ExecuteCustomAction" - x:DataType="vm:ExecuteCustomAction" - Loaded="OnLoaded"> + x:DataType="vm:ExecuteCustomAction"> - + - + - - + + @@ -141,6 +150,28 @@ IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> + + + + + + + + + diff --git a/src/Views/ExecuteCustomAction.axaml.cs b/src/Views/ExecuteCustomAction.axaml.cs index fa7e97139..5589101e0 100644 --- a/src/Views/ExecuteCustomAction.axaml.cs +++ b/src/Views/ExecuteCustomAction.axaml.cs @@ -1,10 +1,8 @@ using System; using Avalonia.Controls; -using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; -using Avalonia.VisualTree; namespace SourceGit.Views { @@ -15,19 +13,6 @@ public ExecuteCustomAction() InitializeComponent(); } - private void OnLoaded(object sender, RoutedEventArgs e) - { - var inputs = this.GetVisualDescendants(); - foreach (var input in inputs) - { - if (input is InputElement { Focusable: true, IsTabStop: true } focusable) - { - focusable.Focus(); - return; - } - } - } - private async void SelectPath(object sender, RoutedEventArgs e) { var topLevel = TopLevel.GetTopLevel(this); @@ -54,7 +39,7 @@ private async void SelectPath(object sender, RoutedEventArgs e) } catch (Exception exception) { - App.RaiseException(string.Empty, $"Failed to select parent folder: {exception.Message}"); + Models.Notification.Send(null, $"Failed to select parent folder: {exception.Message}", true); } } else @@ -62,7 +47,7 @@ private async void SelectPath(object sender, RoutedEventArgs e) var options = new FilePickerOpenOptions() { AllowMultiple = false, - FileTypeFilter = [new FilePickerFileType("File") { Patterns = ["*.*"] }] + FileTypeFilter = [new("SSHKey") { Patterns = ["*"] }] }; var selected = await topLevel.StorageProvider.OpenFilePickerAsync(options); diff --git a/src/Views/BranchCompareCommandPalette.axaml b/src/Views/ExecuteCustomActionCommandPalette.axaml similarity index 61% rename from src/Views/BranchCompareCommandPalette.axaml rename to src/Views/ExecuteCustomActionCommandPalette.axaml index edb37b5d7..e512c2d75 100644 --- a/src/Views/BranchCompareCommandPalette.axaml +++ b/src/Views/ExecuteCustomActionCommandPalette.axaml @@ -2,14 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="SourceGit.Views.BranchCompareCommandPalette" - x:DataType="vm:BranchCompareCommandPalette"> - + x:Class="SourceGit.Views.ExecuteCustomActionCommandPalette" + x:DataType="vm:ExecuteCustomActionCommandPalette"> + - @@ -52,19 +51,25 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/GotoRevisionSelector.axaml.cs b/src/Views/GotoRevisionSelector.axaml.cs new file mode 100644 index 000000000..8483cdbb2 --- /dev/null +++ b/src/Views/GotoRevisionSelector.axaml.cs @@ -0,0 +1,43 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class GotoRevisionSelector : ChromelessWindow + { + public GotoRevisionSelector() + { + CloseOnESC = true; + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + RevisionList.Focus(); + } + + private void OnRevisionListKeyDown(object sender, KeyEventArgs e) + { + if (e is not { Key: Key.Enter, KeyModifiers: KeyModifiers.None }) + return; + + if (sender is not ListBox { SelectedItem: Models.Commit commit }) + return; + + Close(commit); + e.Handled = true; + } + + private void OnRevisionListItemTapped(object sender, TappedEventArgs e) + { + if (sender is not Control { DataContext: Models.Commit commit }) + return; + + Close(commit); + e.Handled = true; + } + } +} + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index c07f88d14..9555d7e21 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -14,7 +14,7 @@ - + @@ -24,37 +24,40 @@ - - - - - + + + + + + + + + + + + @@ -64,12 +82,11 @@ - + + IsVisible="{Binding !#ThisControl.IsScrollButtonVisible}"> @@ -148,12 +165,12 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -255,7 +244,8 @@ Nodes="{Binding LocalBranchTrees}" IsVisible="{Binding IsLocalBranchGroupExpanded}" SelectionChanged="OnLocalBranchTreeSelectionChanged" - RowsChanged="OnBranchTreeRowsChanged"/> + RowsChanged="OnLeftSidebarRowsChanged" + SearchRequested="OnToggleFilter"/> @@ -288,7 +278,8 @@ Nodes="{Binding RemoteBranchTrees}" IsVisible="{Binding IsRemoteGroupExpanded}" SelectionChanged="OnRemoteBranchTreeSelectionChanged" - RowsChanged="OnBranchTreeRowsChanged"/> + RowsChanged="OnLeftSidebarRowsChanged" + SearchRequested="OnToggleFilter"/> @@ -299,10 +290,12 @@ + ToolTip.Tip="{DynamicResource Text.Repository.ShowTagsAsTree}"> + + - -