diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 0845774fb3..9397d78e2b 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -103,6 +103,7 @@ jobs: name: package.${{ matrix.runtime }} path: | build/sourcegit-*.AppImage + build/sourcegit-*.AppImage.zsync build/sourcegit_*.deb build/sourcegit-*.rpm - name: Delete temp artifacts diff --git a/.gitignore b/.gitignore index a0576b9fc6..5ca0482f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.suo *.code-workspace +*.tss .DS_Store .DocumentRevisions-V100 @@ -43,3 +44,4 @@ build/*.AppImage SourceGit.app/ build.command src/Properties/launchSettings.json +*.lscache diff --git a/README.md b/README.md index dd9ffe43b1..2ac5ba0723 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,11 @@ You can download the latest stable from [Releases](https://github.com/sourcegit- This software creates a folder, which is platform-dependent, to store user settings, downloaded avatars and crash logs. -| OS | PATH | -|---------|-------------------------------------------| -| Windows | `%APPDATA%\SourceGit` | -| Linux | `~/.sourcegit` | -| macOS | `~/Library/Application Support/SourceGit` | +| OS | PATH | +|---------|--------------------------------------------------------------------------------------------------| +| Windows | `%APPDATA%\SourceGit` | +| Linux | `${XDG_CONFIG_HOME}/SourceGit` (preference.json) `${XDG_CACHE_HOME}/SourceGit` (avatars & logs) | +| macOS | `~/Library/Application Support/SourceGit` | > [!TIP] > * You can open this data storage directory from the main menu `Open Data Storage Directory`. @@ -120,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 @@ -162,6 +163,14 @@ For **Linux** users: 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. @@ -173,7 +182,7 @@ Users can also launcher `SourceGit` from commandline. Usage: ``` // Open repository in existing `SourceGit` instance or a new one - --file-history // Launch `SourceGit` to see the history of a file + --history // Launch `SourceGit` to see the history of a file or dir --blame // Launch `SourceGit` to blame a file (HEAD version only) ``` @@ -185,6 +194,31 @@ For `OpenAI`: * `Server` must be `https://api.openai.com/v1` +For `OrcaRouter`: + +* `Server` should be `https://api.orcarouter.ai/v1` +* `API Key` should be an OrcaRouter API key (prefixed with `sk-orca-`) +* A good starting `Model` is `orcarouter/auto`, which routes each request to the best available model + +For `Cheaper Inference`: + +* `Server` should be `https://api.cheaperinference.com/v1` +* `API Key` should be a Cheaper Inference API key (prefixed with `ci_live_`) +* A good starting `Model` is `gpt-5.4-mini` + +For `Opper`: + +* `Server` should be `https://api.opper.ai/v3/compat` +* `API Key` should be an Opper API key, created at https://platform.opper.ai +* A good starting `Model` is `gpt-5.4-mini` + +For `API Route`: + +* `Server` should be `https://global.api-route.com/v1` +* `API Key` should be your API Route API key +* `Model` can be `gpt-6.1-sol` if it is available to your key; otherwise select an available chat model from the fetched catalog +* See the [API documentation](https://github.com/DennyHo0917/api-route/blob/main/API.md) for authentication and model discovery + For other AI service: * The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate` @@ -206,7 +240,7 @@ This app supports open repository in external tools listed in the table below. > [!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. +> To solve this problem you can add a file named `external_editors.json` in app data storage directory (config dir for Linux) and provide the path directly. > User can also exclude some editors by using `external_editors.json`. The format of `external_editors.json`: @@ -250,6 +284,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 diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md index f5e0b54005..4be26e4a12 100644 --- a/THIRD-PARTY-LICENSES.md +++ b/THIRD-PARTY-LICENSES.md @@ -7,25 +7,25 @@ The project uses the following third-party libraries or assets ### AvaloniaUI - **Source**: https://github.com/AvaloniaUI/Avalonia -- **Version**: 11.3.13 +- **Version**: 12.0.5 - **License**: MIT License - **License Link**: https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md +### Avalonia.Controls.DataGrid + +- **Source**: https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid +- **Version**: 12.0.1 +- **License**: MIT License +- **License Link**: https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid/blob/master/licence.md + ### AvaloniaEdit - **Official Source**: https://github.com/AvaloniaUI/AvaloniaEdit - **Fork (Modified)**: https://github.com/love-linger/AvaloniaEdit -- **Version**: 11.4.1 +- **Version**: Based on 12.0.x - **License**: MIT License - **License Link**: https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/LICENSE -### LiveChartsCore.SkiaSharpView.Avalonia - -- **Source**: https://github.com/beto-rodriguez/LiveCharts2 -- **Version**: 2.0.0 -- **License**: MIT License -- **License Link**: https://github.com/beto-rodriguez/LiveCharts2/blob/master/LICENSE - ### TextMateSharp - **Source**: https://github.com/danipen/TextMateSharp @@ -61,6 +61,12 @@ The project uses the following third-party libraries or assets - **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 @@ -106,3 +112,38 @@ The project uses the following third-party libraries or assets - **Commit**: 68d98dc57f8486c2946ae28dc86bf8e91d45da4d - **License**: MIT License - **License Link**: https://github.com/vuejs/language-tools/blob/68d98dc57f8486c2946ae28dc86bf8e91d45da4d/LICENSE + +### OCaml + +- **Source**: https://github.com/shentao/vue-multiselect/blob/master/docs/shiki/languages/ocaml.tmLanguage.json +- **Commit**: 8092917041f0ae02cbe4c6982deddc931c6adecd +- **License**: MIT License +- **License Link**: https://github.com/shentao/vue-multiselect/blob/master/LICENSE + +### OCamllex + +- **Source**: https://github.com/textmate/ocaml.tmbundle/blob/master/Syntaxes/OCamllex.tmLanguage +- **Commit**: 1eff26848b39368414cb4214183a2cba22f12d0e +- **License**: Permission to copy, use, modify, sell and distribute this software is granted. This software is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose. +- **License Link**: https://github.com/textmate/ocaml.tmbundle/tree/master#license + +### OCamlyacc + +- **Source**: https://github.com/textmate/ocaml.tmbundle/blob/master/Syntaxes/OCamlyacc.tmLanguage +- **Commit**: 08a892aef6a4b285b1902d9ed1427653fce53e31 +- **License**: Permission to copy, use, modify, sell and distribute this software is granted. This software is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose. +- **License Link**: https://github.com/textmate/ocaml.tmbundle/tree/master#license + +### Erlang + +- **Source**: https://github.com/shentao/vue-multiselect/blob/master/docs/shiki/languages/erlang.tmLanguage.json +- **Commit**: 8092917041f0ae02cbe4c6982deddc931c6adecd +- **License**: MIT License +- **License Link**: https://github.com/shentao/vue-multiselect/blob/master/LICENSE + +### Swift + +- **Source**: https://github.com/jtbandes/swift-tmlanguage/blob/main/Swift.tmLanguage.json +- **Commit**: abda279b567a0a7f35f81642c1b830f437f718d9 +- **License**: MIT License +- **License Link**: https://github.com/jtbandes/swift-tmlanguage/blob/main/LICENSE.md \ No newline at end of file diff --git a/TRANSLATION.md b/TRANSLATION.md index 828f159606..0e33d6f6a4 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,539 +6,804 @@ 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-95.95%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-93.94%25-yellow)
Missing keys in de_DE.axaml -- Text.About.GitSourceRevision -- Text.AIAssistant.Use -- Text.App.HideOthers -- Text.Apply.3Way -- Text.CheckoutBranchFromStash -- Text.CheckoutBranchFromStash.Branch -- Text.CheckoutBranchFromStash.Stash -- Text.Clone.Bookmark -- Text.Clone.Group -- Text.CommandPalette.Branches -- Text.CommandPalette.BranchesAndTags -- Text.CommandPalette.RepositoryActions -- Text.CommandPalette.RevisionFiles -- Text.CommitMessageTextBox.Column -- Text.ConfigureCustomActionControls.StringFormatter -- Text.ConfigureCustomActionControls.StringFormatter.Tip -- Text.ConfigureCustomActionControls.UseFriendlyName -- Text.ConfirmEmptyCommit.StageSelectedThenCommit -- Text.Diff.Submodule.UncommittedChanges -- Text.Discard.IncludeModified -- Text.GotoRevisionSelector -- Text.Hotkeys.Global.OpenLocalRepository -- Text.Hotkeys.Repo.CreateBranch -- Text.Hotkeys.Repo.GoToChild -- Text.Init.CommandTip -- Text.Init.ErrorMessageTip -- Text.OpenLocalRepository -- Text.OpenLocalRepository.Bookmark -- Text.OpenLocalRepository.Group -- Text.OpenLocalRepository.Path -- Text.Preferences.AI.AdditionalPrompt -- Text.Preferences.General.Use24Hours -- Text.RemoteCM.EnableAutoFetch -- Text.StashCM.ApplyFileChanges -- Text.StashCM.Branch -- Text.SubmoduleRevisionCompare -- Text.SubmoduleRevisionCompare.OpenDetails -- Text.Worktree.Branch -- Text.Worktree.Head -- Text.Worktree.Path +- Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CommitDetail.Files.Tips +- Text.Dashboard +- Text.Diff.Binary.ViewContentOfNew +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.ReasoningEffortLevel +- Text.Remote.ResetPushURL +- Text.RemoteCM.SetPushURL +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar
-### ![es__ES](https://img.shields.io/badge/es__ES-99.80%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-93.48%25-yellow)
-Missing keys in es_ES.axaml +Missing keys in el_GR.axaml -- Text.About.GitSourceRevision -- Text.RemoteCM.EnableAutoFetch +- Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CommitDetail.Files.Tips +- Text.Dashboard +- Text.Diff.Binary.ViewContentOfNew +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard +- Text.Launcher.NewVersion +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.ReasoningEffortLevel +- Text.Remote.ResetPushURL +- Text.RemoteCM.SetPushURL +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges
-### ![fr__FR](https://img.shields.io/badge/fr__FR-89.88%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) + +### ![fr__FR](https://img.shields.io/badge/fr__FR-89.75%25-yellow)
Missing keys in fr_FR.axaml -- Text.About.GitSourceRevision -- Text.About.ReleaseDate -- Text.AIAssistant.Use -- Text.App.HideOthers -- Text.Apply.3Way -- Text.Blame.IgnoreWhitespace -- Text.BranchCM.CompareTwo -- Text.BranchCM.CompareWith -- Text.BranchCM.CompareWithHead -- Text.BranchCM.EditDescription -- Text.ChangeCM.Merge -- Text.ChangeCM.MergeExternal -- Text.ChangeCM.ResetFileTo -- Text.Checkout.WarnUpdatingSubmodules -- Text.CheckoutBranchFromStash -- Text.CheckoutBranchFromStash.Branch -- Text.CheckoutBranchFromStash.Stash -- Text.Clone.Bookmark -- Text.Clone.Group -- Text.CommandPalette.Branches -- Text.CommandPalette.BranchesAndTags -- Text.CommandPalette.RepositoryActions -- Text.CommandPalette.RevisionFiles -- Text.CommitMessageTextBox.Column -- Text.CommitMessageTextBox.Placeholder -- Text.Compare.WithHead -- Text.Configure.Git.AskBeforeAutoUpdatingSubmodules -- Text.ConfigureCustomActionControls.StringFormatter -- Text.ConfigureCustomActionControls.StringFormatter.Tip -- Text.ConfigureCustomActionControls.UseFriendlyName -- Text.ConfirmEmptyCommit.StageSelectedThenCommit -- Text.Diff.Submodule.UncommittedChanges -- Text.Discard.IncludeModified -- Text.EditBranchDescription -- Text.EditBranchDescription.Target -- Text.FileCM.CustomAction -- Text.GotoRevisionSelector -- Text.Histories.Header.DateTime -- Text.Histories.ShowColumns -- Text.Hotkeys.Global.OpenLocalRepository -- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu -- Text.Hotkeys.Global.Zoom -- Text.Hotkeys.Repo.CreateBranch -- Text.Hotkeys.Repo.GoToChild -- Text.Hotkeys.Repo.GoToParent -- Text.Init.CommandTip -- Text.Init.ErrorMessageTip -- 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.No -- 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.DiffMerge.DiffArgs -- Text.Preferences.DiffMerge.DiffArgs.Tip -- Text.Preferences.DiffMerge.MergeArgs -- Text.Preferences.DiffMerge.MergeArgs.Tip -- Text.Preferences.General.Use24Hours -- Text.Preferences.Shell.Args -- Text.Preferences.Shell.Args.Tip -- Text.RemoteCM.EnableAutoFetch -- Text.Repository.OpenAsFolder -- Text.Repository.Resolve -- Text.SelfUpdate.CurrentVersion -- Text.SelfUpdate.ReleaseDate -- Text.SquashOrFixup.Squash -- Text.SquashOrFixup.Fixup -- Text.SquashOrFixup.Into -- Text.StashCM.ApplyFileChanges -- Text.StashCM.Branch -- Text.SubmoduleRevisionCompare -- Text.SubmoduleRevisionCompare.OpenDetails -- Text.TagCM.CompareTwo -- Text.TagCM.CompareWith -- Text.TagCM.CompareWithHead -- Text.WorkingCopy.Conflicts.Merge -- Text.WorkingCopy.Conflicts.MergeExternal -- Text.Worktree.Branch -- Text.Worktree.Head -- Text.Worktree.Path -- Text.Yes +- Text.Apply.Source +- Text.Apply.Source.File +- Text.Apply.Source.Clipboard +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.CommitDetail.Files.Tips +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.CopyAsPatch +- Text.Dashboard +- Text.DeleteBranch.Force +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- 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.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard +- Text.Launcher.NewVersion +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.ReasoningEffortLevel +- Text.Preferences.General.UseCompactBranchNames +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts +- Text.Remote.ResetPushURL +- Text.RemoteCM.SetPushURL +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges
-### ![id__ID](https://img.shields.io/badge/id__ID-87.75%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-89.75%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.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.CommitDetail.Files.Tips +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.CopyAsPatch +- Text.Dashboard +- Text.DeleteBranch.Force +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- 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.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard +- Text.Launcher.NewVersion +- Text.Merge.Test +- Text.Merge.Test.NoConflicts +- Text.Merge.Test.UnknownError +- Text.Merge.Test.WillCauseConflicts +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.ReasoningEffortLevel +- Text.Preferences.General.UseCompactBranchNames +- Text.Rebase.Test +- Text.Rebase.Test.OK +- Text.Rebase.Test.UnknownError +- Text.Rebase.Test.WillCauseConflicts +- Text.Remote.ResetPushURL +- Text.RemoteCM.SetPushURL +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges + +
+ +### ![id__ID](https://img.shields.io/badge/id__ID-93.94%25-yellow)
Missing keys in id_ID.axaml -- Text.About.GitSourceRevision -- Text.About.ReleaseDate -- Text.About.ReleaseNotes -- Text.AIAssistant.Use -- Text.App.HideOthers -- Text.Apply.3Way -- Text.Blame.BlameOnPreviousRevision -- Text.Blame.IgnoreWhitespace -- Text.BranchCM.CompareTwo -- Text.BranchCM.CompareWith -- Text.BranchCM.CompareWithHead -- Text.BranchCM.CreatePR -- Text.BranchCM.CreatePRForUpstream -- Text.BranchCM.EditDescription -- Text.ChangeCM.Merge -- Text.ChangeCM.MergeExternal -- Text.ChangeCM.ResetFileTo -- Text.Checkout.WarnUpdatingSubmodules -- Text.CheckoutBranchFromStash -- Text.CheckoutBranchFromStash.Branch -- Text.CheckoutBranchFromStash.Stash -- Text.Clone.Bookmark -- Text.Clone.Group -- Text.CommandPalette.Branches -- Text.CommandPalette.BranchesAndTags -- Text.CommandPalette.RepositoryActions -- Text.CommandPalette.RevisionFiles -- Text.CommitCM.Drop -- Text.CommitMessageTextBox.Column -- Text.CommitMessageTextBox.Placeholder -- Text.Compare.WithHead -- Text.Configure.CommitMessageTemplate.BuiltinVars -- Text.Configure.Git.AskBeforeAutoUpdatingSubmodules -- Text.Configure.Git.ConventionalTypesOverride -- Text.ConfigureCustomActionControls.StringFormatter -- Text.ConfigureCustomActionControls.StringFormatter.Tip -- Text.ConfigureCustomActionControls.StringValue.Tip -- Text.ConfigureCustomActionControls.UseFriendlyName -- Text.ConfirmEmptyCommit.StageSelectedThenCommit -- Text.DealWithLocalChanges.DoNothing -- Text.Diff.Submodule.UncommittedChanges -- Text.Discard.IncludeModified -- 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.GotoRevisionSelector -- Text.Histories.Header.DateTime -- Text.Histories.ShowColumns -- Text.Hotkeys.Global.OpenLocalRepository -- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu -- Text.Hotkeys.Global.Zoom -- Text.Hotkeys.Repo.CreateBranch -- Text.Hotkeys.Repo.GoToChild -- Text.Hotkeys.Repo.GoToParent -- Text.Hotkeys.Repo.OpenCommandPalette -- Text.Init.CommandTip -- Text.Init.ErrorMessageTip -- Text.Launcher.Commands -- Text.Launcher.OpenRepository -- 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.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.DiffMerge.DiffArgs -- Text.Preferences.DiffMerge.DiffArgs.Tip -- Text.Preferences.DiffMerge.MergeArgs -- Text.Preferences.DiffMerge.MergeArgs.Tip -- Text.Preferences.General.Use24Hours -- Text.Preferences.Shell.Args -- Text.Preferences.Shell.Args.Tip -- Text.PushToNewBranch -- Text.PushToNewBranch.Title -- Text.RemoteCM.EnableAutoFetch -- Text.Repository.OpenAsFolder -- Text.Repository.Resolve -- Text.SelfUpdate.CurrentVersion -- Text.SelfUpdate.ReleaseDate -- Text.SquashOrFixup.Squash -- Text.SquashOrFixup.Fixup -- Text.SquashOrFixup.Into -- Text.StashCM.ApplyFileChanges -- Text.StashCM.Branch -- Text.SubmoduleRevisionCompare -- Text.SubmoduleRevisionCompare.OpenDetails -- Text.TagCM.CompareTwo -- Text.TagCM.CompareWith -- Text.TagCM.CompareWithHead -- Text.WorkingCopy.Conflicts.Merge -- Text.WorkingCopy.Conflicts.MergeExternal -- Text.Worktree.Branch -- Text.Worktree.Head -- Text.Worktree.Path -- Text.Yes +- Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CommitDetail.Files.Tips +- Text.Dashboard +- Text.Diff.Binary.ViewContentOfNew +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config +- Text.Preferences.AI.ReasoningEffortLevel +- Text.Remote.ResetPushURL +- Text.RemoteCM.SetPushURL +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges
-### ![it__IT](https://img.shields.io/badge/it__IT-95.34%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-83.78%25-yellow)
Missing keys in it_IT.axaml -- Text.About.GitSourceRevision - 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.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchCM.CompareWithSpecial +- Text.BranchTree.PushURL - Text.ChangeCM.ResetFileTo +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - 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.CommitMessageTextBox.Column +- Text.CommitCM.CopyAuthorTime +- Text.CommitCM.CopyCommitterTime +- Text.CommitDetail.CollapseToBottom +- Text.CommitDetail.Files.Tips +- 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.Dashboard +- Text.DeleteBranch.Force +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile - Text.Diff.Submodule.UncommittedChanges - Text.Discard.IncludeModified +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- 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.Header.DateTime +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent - 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.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard - 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.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config - Text.OpenLocalRepository - Text.OpenLocalRepository.Bookmark - Text.OpenLocalRepository.Group - Text.OpenLocalRepository.Path - Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels +- Text.Preferences.AI.ReasoningEffortLevel +- 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.Remote.ResetPushURL - Text.RemoteCM.EnableAutoFetch +- Text.RemoteCM.SetPushURL +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip - Text.SelfUpdate.CurrentVersion - Text.SelfUpdate.ReleaseDate +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.SubmoduleRevisionCompare - Text.SubmoduleRevisionCompare.OpenDetails +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges - Text.Worktree.Branch - Text.Worktree.Head - Text.Worktree.Path
-### ![ja__JP](https://img.shields.io/badge/ja__JP-96.26%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-97.95%25-yellow)
Missing keys in ja_JP.axaml -- Text.About.GitSourceRevision -- Text.AIAssistant.Use -- Text.App.HideOthers -- Text.Apply.3Way -- Text.CheckoutBranchFromStash -- Text.CheckoutBranchFromStash.Branch -- Text.CheckoutBranchFromStash.Stash -- Text.Clone.Bookmark -- Text.Clone.Group -- Text.CommandPalette.Branches -- Text.CommandPalette.BranchesAndTags -- Text.CommandPalette.RepositoryActions -- Text.CommandPalette.RevisionFiles -- Text.ConfigureCustomActionControls.StringFormatter -- Text.ConfigureCustomActionControls.StringFormatter.Tip -- Text.ConfigureCustomActionControls.UseFriendlyName -- Text.ConfirmEmptyCommit.StageSelectedThenCommit -- Text.DealWithLocalChanges.DoNothing -- Text.Diff.Submodule.UncommittedChanges -- Text.Discard.IncludeModified -- Text.Hotkeys.Global.OpenLocalRepository -- Text.Hotkeys.Repo.CreateBranch -- Text.Init.CommandTip -- Text.Init.ErrorMessageTip -- Text.OpenLocalRepository -- Text.OpenLocalRepository.Bookmark -- Text.OpenLocalRepository.Group -- Text.OpenLocalRepository.Path -- Text.Preferences.AI.AdditionalPrompt -- Text.Preferences.General.Use24Hours -- Text.RemoteCM.EnableAutoFetch -- Text.StashCM.Branch -- Text.SubmoduleRevisionCompare -- Text.SubmoduleRevisionCompare.OpenDetails -- Text.Worktree.Branch -- Text.Worktree.Head -- Text.Worktree.Path +- Text.BranchCM.CheckoutAsWorktree +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CommitDetail.Files.Tips +- Text.Statistics.Commits +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.ExpandSidebar
-### ![ko__KR](https://img.shields.io/badge/ko__KR-88.06%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-91.15%25-yellow)
Missing keys in ko_KR.axaml -- Text.About.GitSourceRevision -- Text.About.ReleaseDate -- Text.AIAssistant.Use -- Text.App.HideOthers -- Text.Apply.3Way -- Text.Blame.BlameOnPreviousRevision -- Text.Blame.IgnoreWhitespace -- Text.Blame.TypeNotSupported -- Text.BranchCM.CompareTwo -- Text.BranchCM.CompareWith -- Text.BranchCM.CompareWithHead -- Text.BranchCM.CreatePR -- Text.BranchCM.CreatePRForUpstream -- Text.BranchCM.EditDescription -- Text.ChangeCM.Merge -- Text.ChangeCM.MergeExternal -- Text.ChangeCM.ResetFileTo -- Text.Checkout.WarnUpdatingSubmodules -- Text.CheckoutBranchFromStash -- Text.CheckoutBranchFromStash.Branch -- Text.CheckoutBranchFromStash.Stash -- Text.Clone.Bookmark -- Text.Clone.Group -- Text.CommandPalette.Branches -- Text.CommandPalette.BranchesAndTags -- Text.CommandPalette.RepositoryActions -- Text.CommandPalette.RevisionFiles -- Text.CommitMessageTextBox.Column -- Text.CommitMessageTextBox.Placeholder -- Text.Compare.WithHead -- Text.Configure.Git.AskBeforeAutoUpdatingSubmodules -- Text.Configure.Git.ConventionalTypesOverride -- Text.ConfigureCustomActionControls.StringFormatter -- Text.ConfigureCustomActionControls.StringFormatter.Tip -- Text.ConfigureCustomActionControls.StringValue.Tip -- Text.ConfigureCustomActionControls.UseFriendlyName -- Text.ConfirmEmptyCommit.StageSelectedThenCommit -- Text.DealWithLocalChanges.DoNothing -- Text.Diff.Submodule.UncommittedChanges -- Text.Discard.IncludeModified -- Text.EditBranchDescription -- Text.EditBranchDescription.Target -- Text.FileCM.CustomAction -- Text.GitLFS.Locks.UnlockAllMyLocks -- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm -- Text.GotoRevisionSelector -- Text.Histories.Header.DateTime -- Text.Histories.ShowColumns -- Text.Hotkeys.Global.OpenLocalRepository -- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu -- Text.Hotkeys.Global.Zoom -- Text.Hotkeys.Repo.CreateBranch -- Text.Hotkeys.Repo.GoToChild -- Text.Hotkeys.Repo.GoToParent -- Text.Hotkeys.Repo.OpenCommandPalette -- Text.Init.CommandTip -- Text.Init.ErrorMessageTip -- Text.Launcher.Commands -- Text.Launcher.OpenRepository -- 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.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.Appearance.UseFixedTabWidth -- Text.Preferences.DiffMerge.DiffArgs -- Text.Preferences.DiffMerge.DiffArgs.Tip -- Text.Preferences.DiffMerge.MergeArgs -- Text.Preferences.DiffMerge.MergeArgs.Tip -- Text.Preferences.General.Use24Hours -- Text.Preferences.Shell.Args -- Text.Preferences.Shell.Args.Tip -- Text.PushToNewBranch -- Text.PushToNewBranch.Title -- Text.RemoteCM.EnableAutoFetch -- Text.Repository.OpenAsFolder -- Text.Repository.Resolve -- Text.SelfUpdate.CurrentVersion -- Text.SelfUpdate.ReleaseDate -- Text.SquashOrFixup.Squash -- Text.SquashOrFixup.Fixup -- Text.SquashOrFixup.Into -- Text.StashCM.ApplyFileChanges -- Text.StashCM.Branch -- Text.Submodule.Status.Unmerged -- Text.SubmoduleRevisionCompare -- Text.SubmoduleRevisionCompare.OpenDetails -- Text.TagCM.CompareTwo -- Text.TagCM.CompareWith -- Text.TagCM.CompareWithHead -- Text.WorkingCopy.Conflicts.Merge -- Text.WorkingCopy.Conflicts.MergeExternal -- Text.Worktree.Branch -- Text.Worktree.Head -- Text.Worktree.Path -- Text.Yes +- Text.Bisect.WaitingForCheckoutAnother +- Text.Bisect.WaitingForFirstBad +- Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder +- Text.CheckoutDetached +- Text.CheckoutDetached.Target +- Text.CheckoutDetached.Warning +- Text.CommitDetail.Files.Tips +- Text.Configure.Git.EnableRecursiveWhenAutoUpdatingSubmodules +- Text.Dashboard +- Text.DeleteBranch.Force +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress +- 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.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard +- Text.Launcher.NewVersion +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.ReasoningEffortLevel +- Text.Remote.ResetPushURL +- Text.RemoteCM.SetPushURL +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL +- Text.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip +- Text.TagCM.Checkout +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- Text.UpdateSubmodules.Recursive +- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.CollapseSidebar +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges
-### ![pt__BR](https://img.shields.io/badge/pt__BR-66.70%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-58.99%25-red)
Missing keys in pt_BR.axaml -- Text.About.GitSourceRevision - 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.Blame.BlameOnPreviousRevision +- Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree +- Text.BranchCM.CompareWithSpecial - Text.BranchCM.InteractiveRebase.Manually - Text.BranchTree.AheadBehind - Text.BranchTree.Behind +- Text.BranchTree.PushURL - Text.BranchTree.Tracking - Text.BranchTree.URL - Text.BranchTree.Worktree @@ -552,9 +817,20 @@ This document shows the translation status of each locale file in the repository - Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - 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 @@ -563,10 +839,11 @@ This document shows the translation status of each locale file in the repository - 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 @@ -574,24 +851,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.Files.Tips - 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.WithHead +- 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 @@ -603,6 +882,7 @@ This document shows the translation status of each locale file in the repository - 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 @@ -629,17 +909,22 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.WithLocalChanges - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message +- Text.CopyAsPatch - Text.CopyFullPath - Text.CreateBranch.OverwriteExisting +- Text.Dashboard - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteBranch.Force - Text.DeleteMultiTags - Text.DeleteMultiTags.DeleteFromRemotes - Text.DeleteMultiTags.Tip - Text.DeleteRepositoryNode.Path - Text.DeleteRepositoryNode.TipForGroup - Text.DeleteRepositoryNode.TipForRepository +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile - Text.Diff.First - Text.Diff.Image.Blend - Text.Diff.Image.Difference @@ -656,22 +941,44 @@ This document shows the translation status of each locale file in the repository - 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.Fetch.Force +- Text.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress - Text.FileCM.CustomAction - Text.FileCM.ResolveUsing +- 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.Header.DateTime +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent - Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare - Text.Hotkeys.Global.Clone - Text.Hotkeys.Global.OpenLocalRepository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu @@ -681,20 +988,28 @@ This document shows the translation status of each locale file in the repository - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette -- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard - 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 @@ -721,6 +1036,8 @@ This document shows the translation status of each locale file in the repository - Text.No - Text.Open - Text.Open.SystemDefaultEditor +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config - Text.OpenFile - Text.OpenLocalRepository - Text.OpenLocalRepository.Bookmark @@ -729,7 +1046,11 @@ This document shows the translation status of each locale file in the repository - Text.PageTabBar.Tab.MoveToWorkspace - Text.PageTabBar.Tab.Refresh - Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels - Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.AI.ReasoningEffortLevel - Text.Preferences.Appearance.EditorTabWidth - Text.Preferences.Appearance.UseAutoHideScrollBars - Text.Preferences.DiffMerge.DiffArgs @@ -740,13 +1061,15 @@ 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.Push.New @@ -754,25 +1077,34 @@ This document shows the translation status of each locale file in the repository - 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.Remote.ResetPushURL - Text.RemoteCM.CustomAction - Text.RemoteCM.EnableAutoFetch +- Text.RemoteCM.SetPushURL - 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.Search.Method +- Text.Repository.Search.ToolTip - Text.Repository.ShowDecoratedCommitsOnly - Text.Repository.ShowFirstParentOnly - Text.Repository.ShowFlags @@ -782,7 +1114,6 @@ 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 @@ -791,6 +1122,10 @@ This document shows the translation status of each locale file in the repository - Text.ScanRepositories.UseCustomDir - Text.SelfUpdate.CurrentVersion - Text.SelfUpdate.ReleaseDate +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL - Text.SetSubmoduleBranch - Text.SetSubmoduleBranch.Submodule - Text.SetSubmoduleBranch.Current @@ -801,14 +1136,33 @@ 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.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title - Text.Stash.Mode - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch - Text.StashCM.CopyMessage - Text.StashCM.SaveAsPatch +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -827,6 +1181,7 @@ This document shows the translation status of each locale file in the repository - Text.SubmoduleRevisionCompare.OpenDetails - Text.Tag.Tagger - Text.Tag.Time +- Text.TagCM.Checkout - Text.TagCM.CompareTwo - Text.TagCM.CompareWith - Text.TagCM.CompareWithHead @@ -836,14 +1191,24 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.CopyName - Text.TagCM.CustomAction - Text.TagCM.DeleteMultiple +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- 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.CollapseSidebar - Text.WorkingCopy.CommitToEdit - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.ConfirmCommitWithFilter @@ -852,6 +1217,9 @@ This document shows the translation status of each locale file in the repository - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.WorkingCopy.SignOff @@ -863,21 +1231,13 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.90%25-yellow) - -
-Missing keys in ru_RU.axaml - -- Text.About.GitSourceRevision - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) -### ![ta__IN](https://img.shields.io/badge/ta__IN-68.72%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-60.58%25-red)
Missing keys in ta_IN.axaml -- Text.About.GitSourceRevision - Text.About.ReleaseDate - Text.About.ReleaseNotes - Text.AddToIgnore @@ -888,20 +1248,28 @@ This document shows the translation status of each locale file in the repository - 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.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchCM.CompareTwo - Text.BranchCM.CompareWith - Text.BranchCM.CompareWithHead +- Text.BranchCM.CompareWithSpecial - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.EditDescription @@ -912,6 +1280,7 @@ This document shows the translation status of each locale file in the repository - Text.BranchTree.AheadBehind - Text.BranchTree.Behind - Text.BranchTree.InvalidUpstream +- Text.BranchTree.PushURL - Text.BranchTree.Remote - Text.BranchTree.Status - Text.BranchTree.Tracking @@ -927,9 +1296,20 @@ This document shows the translation status of each locale file in the repository - Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - 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 @@ -937,10 +1317,11 @@ This document shows the translation status of each locale file in the repository - 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 @@ -951,17 +1332,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.Files.Tips - 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.WithHead +- 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 @@ -971,6 +1356,7 @@ This document shows the translation status of each locale file in the repository - 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 @@ -995,14 +1381,19 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.WithLocalChanges - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message +- Text.CopyAsPatch - Text.CreateBranch.OverwriteExisting +- Text.Dashboard - Text.DealWithLocalChanges.DoNothing - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteBranch.Force - Text.DeleteMultiTags - Text.DeleteMultiTags.DeleteFromRemotes - Text.DeleteMultiTags.Tip +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile - Text.Diff.Image.Blend - Text.Diff.Image.Difference - Text.Diff.Image.SideBySide @@ -1017,20 +1408,42 @@ This document shows the translation status of each locale file in the repository - 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.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress - Text.FileCM.CustomAction +- 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.Header.DateTime +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent - Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare - Text.Hotkeys.Global.OpenLocalRepository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab @@ -1039,15 +1452,23 @@ This document shows the translation status of each locale file in the repository - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette -- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard - 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 @@ -1070,6 +1491,8 @@ This document shows the translation status of each locale file in the repository - Text.No - Text.Open - Text.Open.SystemDefaultEditor +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config - Text.OpenFile - Text.OpenLocalRepository - Text.OpenLocalRepository.Bookmark @@ -1078,7 +1501,11 @@ This document shows the translation status of each locale file in the repository - Text.PageTabBar.Tab.MoveToWorkspace - Text.PageTabBar.Tab.Refresh - Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels - Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.AI.ReasoningEffortLevel - Text.Preferences.Appearance.UseAutoHideScrollBars - Text.Preferences.DiffMerge.DiffArgs - Text.Preferences.DiffMerge.DiffArgs.Tip @@ -1087,10 +1514,13 @@ 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.Push.New @@ -1098,25 +1528,33 @@ This document shows the translation status of each locale file in the repository - 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.Remote.ResetPushURL - Text.RemoteCM.CustomAction - Text.RemoteCM.EnableAutoFetch +- Text.RemoteCM.SetPushURL - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes -- Text.Repository.Dashboard -- Text.Repository.MoreOptions -- Text.Repository.OnlyHighlightCurrentBranchInGraph +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary - Text.Repository.OpenAsFolder - Text.Repository.Resolve - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip - 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 @@ -1125,18 +1563,41 @@ This document shows the translation status of each locale file in the repository - Text.ScanRepositories.UseCustomDir - Text.SelfUpdate.CurrentVersion - Text.SelfUpdate.ReleaseDate +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL - 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.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title - Text.Stash.Mode - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch - Text.StashCM.CopyMessage +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -1155,6 +1616,7 @@ This document shows the translation status of each locale file in the repository - Text.SubmoduleRevisionCompare.OpenDetails - Text.Tag.Tagger - Text.Tag.Time +- Text.TagCM.Checkout - Text.TagCM.CompareTwo - Text.TagCM.CompareWith - Text.TagCM.CompareWithHead @@ -1164,21 +1626,34 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.CopyName - Text.TagCM.CustomAction - Text.TagCM.DeleteMultiple +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - 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.CollapseSidebar - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.Conflicts.Merge - Text.WorkingCopy.Conflicts.MergeExternal - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.Worktree.Branch @@ -1189,12 +1664,11 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-69.53%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.32%25-red)
Missing keys in uk_UA.axaml -- Text.About.GitSourceRevision - Text.About.ReleaseDate - Text.About.ReleaseNotes - Text.AddToIgnore @@ -1205,20 +1679,28 @@ This document shows the translation status of each locale file in the repository - 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.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchCM.CompareTwo - Text.BranchCM.CompareWith - Text.BranchCM.CompareWithHead +- Text.BranchCM.CompareWithSpecial - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.EditDescription @@ -1229,6 +1711,7 @@ This document shows the translation status of each locale file in the repository - Text.BranchTree.AheadBehind - Text.BranchTree.Behind - Text.BranchTree.InvalidUpstream +- Text.BranchTree.PushURL - Text.BranchTree.Remote - Text.BranchTree.Status - Text.BranchTree.Tracking @@ -1244,9 +1727,20 @@ This document shows the translation status of each locale file in the repository - Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - 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 @@ -1254,10 +1748,11 @@ This document shows the translation status of each locale file in the repository - 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 @@ -1268,17 +1763,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.Files.Tips - 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.WithHead +- 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 @@ -1288,6 +1787,7 @@ This document shows the translation status of each locale file in the repository - 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 @@ -1308,14 +1808,19 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.StageSelectedThenCommit - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message +- Text.CopyAsPatch - Text.CreateBranch.OverwriteExisting +- Text.Dashboard - Text.DealWithLocalChanges.DoNothing - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteBranch.Force - Text.DeleteMultiTags - Text.DeleteMultiTags.DeleteFromRemotes - Text.DeleteMultiTags.Tip +- Text.Diff.Binary.ViewContentOfNew +- Text.Diff.EmptyFile - Text.Diff.Image.Blend - Text.Diff.Image.Difference - Text.Diff.Image.SideBySide @@ -1330,20 +1835,42 @@ This document shows the translation status of each locale file in the repository - 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.File.Binary +- Text.File.HexViewer +- Text.File.HexViewer.GotoAddress - Text.FileCM.CustomAction +- 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.Header.DateTime +- Text.Histories.AdvancedOptions +- Text.Histories.HighlightsInGraph +- Text.Histories.HighlightsInGraph.All +- Text.Histories.HighlightsInGraph.CurrentBranchOnly +- Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits +- Text.Histories.HighlightsInGraph.SelectedCommitsOnly +- Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent - Text.Histories.ShowColumns +- Text.HistoriesDetailsStandalone +- Text.HistoriesDetailsStandalone.CommitDetail +- Text.HistoriesDetailsStandalone.RevisionCompare - Text.Hotkeys.Global.OpenLocalRepository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab @@ -1352,15 +1879,23 @@ This document shows the translation status of each locale file in the repository - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette -- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Hotkeys.Repo.ToggleCommitDetailPanel +- Text.Hotkeys.Repo.ToggleSearchCommits +- Text.Hotkeys.Repo.ViewDashboard - 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 @@ -1383,6 +1918,8 @@ This document shows the translation status of each locale file in the repository - Text.No - Text.Open - Text.Open.SystemDefaultEditor +- Text.OpenAppDataDir.Cache +- Text.OpenAppDataDir.Config - Text.OpenFile - Text.OpenLocalRepository - Text.OpenLocalRepository.Bookmark @@ -1391,7 +1928,11 @@ This document shows the translation status of each locale file in the repository - Text.PageTabBar.Tab.MoveToWorkspace - Text.PageTabBar.Tab.Refresh - Text.Preferences.AI.AdditionalPrompt +- Text.Preferences.AI.ExtraHeaders +- Text.Preferences.AI.Model +- Text.Preferences.AI.Model.AutoFetchAvailableModels - Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.AI.ReasoningEffortLevel - Text.Preferences.Appearance.UseAutoHideScrollBars - Text.Preferences.DiffMerge.DiffArgs - Text.Preferences.DiffMerge.DiffArgs.Tip @@ -1400,10 +1941,13 @@ 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.Push.New @@ -1411,25 +1955,33 @@ This document shows the translation status of each locale file in the repository - 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.Remote.ResetPushURL - Text.RemoteCM.CustomAction - Text.RemoteCM.EnableAutoFetch +- Text.RemoteCM.SetPushURL - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes -- Text.Repository.Dashboard -- Text.Repository.MoreOptions -- Text.Repository.OnlyHighlightCurrentBranchInGraph +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Expand +- Text.Repository.FilterCommits.Summary - Text.Repository.OpenAsFolder - Text.Repository.Resolve - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath +- Text.Repository.Search.Method +- Text.Repository.Search.ToolTip - 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 @@ -1438,18 +1990,41 @@ This document shows the translation status of each locale file in the repository - Text.ScanRepositories.UseCustomDir - Text.SelfUpdate.CurrentVersion - Text.SelfUpdate.ReleaseDate +- Text.SetPushUrl +- Text.SetPushUrl.PushURL +- Text.SetPushUrl.Remote +- Text.SetPushUrl.URL - 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.SSHKeyHelper +- Text.SSHKeyHelper.ConfirmDeletion +- Text.SSHKeyHelper.Fingerprint +- Text.SSHKeyHelper.Generator +- Text.SSHKeyHelper.Generator.ConfirmPassphrase +- Text.SSHKeyHelper.Generator.Email +- Text.SSHKeyHelper.Generator.Email.Placeholder +- Text.SSHKeyHelper.Generator.Name +- Text.SSHKeyHelper.Generator.Name.Placeholder +- Text.SSHKeyHelper.Generator.Passphrase +- Text.SSHKeyHelper.Generator.Start +- Text.SSHKeyHelper.Generator.Type +- Text.SSHKeyHelper.Generator.UsePassphrase +- Text.SSHKeyHelper.Path +- Text.SSHKeyHelper.PublicKey +- Text.SSHKeyHelper.Title - Text.Stash.Mode - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch - Text.StashCM.CopyMessage +- Text.Statistics.Commits +- Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -1468,6 +2043,7 @@ This document shows the translation status of each locale file in the repository - Text.SubmoduleRevisionCompare.OpenDetails - Text.Tag.Tagger - Text.Tag.Time +- Text.TagCM.Checkout - Text.TagCM.CompareTwo - Text.TagCM.CompareWith - Text.TagCM.CompareWithHead @@ -1477,17 +2053,30 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.CopyName - Text.TagCM.CustomAction - Text.TagCM.DeleteMultiple +- Text.TagCM.Merge +- Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped +- 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.CollapseSidebar - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.Conflicts.Merge - Text.WorkingCopy.Conflicts.MergeExternal +- Text.WorkingCopy.DiscardAll +- Text.WorkingCopy.ExpandSidebar +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.Worktree.Branch diff --git a/VERSION b/VERSION index a1cc9a833a..bb71f1638b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026.10 \ No newline at end of file +2026.22 \ No newline at end of file diff --git a/build/resources/_common/applications/sourcegit.desktop b/build/resources/_common/applications/sourcegit.desktop index bcf9c813ca..966b1d65f3 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 ba6f40a2bb..d20efa5e29 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/flatpak/sourcegit.desktop b/build/resources/flatpak/sourcegit.desktop index f1213e6f6d..9cf9665be9 100644 --- a/build/resources/flatpak/sourcegit.desktop +++ b/build/resources/flatpak/sourcegit.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=SourceGit Comment=Open-source & Free Git GUI Client -Exec=sourcegit.sh +Exec=sourcegit Icon=io.github.sourcegit_scm.sourcegit Terminal=false Type=Application diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh index 262f141ae8..e4645b7213 100755 --- a/build/scripts/package.linux.sh +++ b/build/scripts/package.linux.sh @@ -51,7 +51,9 @@ ln -rsf SourceGit.AppDir/opt/sourcegit/sourcegit SourceGit.AppDir/AppRun ln -rsf SourceGit.AppDir/usr/share/applications/com.sourcegit_scm.SourceGit.desktop SourceGit.AppDir cp resources/appimage/sourcegit.appdata.xml SourceGit.AppDir/usr/share/metainfo/com.sourcegit_scm.SourceGit.appdata.xml -ARCH="$appimage_arch" ./appimagetool -v SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage" +ARCH="$appimage_arch" ./appimagetool -v \ + -u "gh-releases-zsync|sourcegit-scm|sourcegit|latest|sourcegit-*.linux.$arch.AppImage.zsync" \ + SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage" mkdir -p resources/deb/opt/sourcegit/ mkdir -p resources/deb/usr/bin diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh index 8f3ddc77c8..78c31114f4 100755 --- a/build/scripts/package.osx-app.sh +++ b/build/scripts/package.osx-app.sh @@ -14,4 +14,14 @@ sed "s/SOURCE_GIT_VERSION/$VERSION/g" resources/app/App.plist > SourceGit.app/Co rm -rf SourceGit.app/Contents/MacOS/SourceGit.dsym rm -f SourceGit.app/Contents/MacOS/*.pdb +case "$RUNTIME" in + osx-arm64) ARCH=arm64 ;; + osx-x64) ARCH=x86_64 ;; + *) echo "Unsupported RUNTIME: $RUNTIME" >&2; exit 1 ;; +esac + +clang -arch "$ARCH" ../tools/setsid-macos/setsid.c -o SourceGit.app/Contents/MacOS/setsid -mmacosx-version-min=13.0 +codesign --force --deep --sign - SourceGit.app +codesign --verify --deep --strict --verbose=2 SourceGit.app + zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit.app diff --git a/depends/AvaloniaEdit b/depends/AvaloniaEdit index 1e6f595ff0..ddf00a91dc 160000 --- a/depends/AvaloniaEdit +++ b/depends/AvaloniaEdit @@ -1 +1 @@ -Subproject commit 1e6f595ff0da0dc20637322cbab9531892c12cbc +Subproject commit ddf00a91dc0da29b85c79b85c9583860213a99ff diff --git a/screenshots/theme_dark.png b/screenshots/theme_dark.png index 6e8af07b38..bd8fcf339e 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 10988efdb3..baa54ef722 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 index c551fd34e8..178683ec68 100644 --- a/src/AI/Agent.cs +++ b/src/AI/Agent.cs @@ -14,17 +14,17 @@ public Agent(Service service) _service = service; } - public async Task GenerateCommitMessageAsync(string repo, string changeList, Action onUpdate, CancellationToken cancellation) + 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 } }; -#pragma warning disable SCME0001 - options.Patch.Set("$.thinking"u8, Encoding.UTF8.GetBytes("""{"type": "disabled"}""")); - options.Patch.Set("$.enable_thinking"u8, false); -#pragma warning restore SCME0001 +#pragma warning disable OPENAI001 + if (!_service.ReasoningEffortLevel.Equals(Options.IgnoredReasoningEffortLevel, StringComparison.OrdinalIgnoreCase)) + options.ReasoningEffortLevel = new ChatReasoningEffortLevel(_service.ReasoningEffortLevel); +#pragma warning restore OPENAI001 var userMessageBuilder = new StringBuilder(); userMessageBuilder @@ -33,6 +33,7 @@ public async Task GenerateCommitMessageAsync(string repo, string changeList, Act .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); @@ -46,26 +47,60 @@ public async Task GenerateCommitMessageAsync(string repo, string changeList, Act switch (completion.FinishReason) { case ChatFinishReason.Stop: - onUpdate?.Invoke(string.Empty); - onUpdate?.Invoke("# Assistant"); - if (completion.Content.Count > 0) - onUpdate?.Invoke(completion.Content[0].Text); - else - onUpdate?.Invoke("[No content was generated.]"); + 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}"); + 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: { - messages.Add(new AssistantChatMessage(completion)); + 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, onUpdate); + var result = await ChatTools.ProcessAsync(call, repo, amendParent, onUpdate); messages.Add(result); } diff --git a/src/AI/ChatTools.cs b/src/AI/ChatTools.cs index b9e2f80c6c..57a8e14595 100644 --- a/src/AI/ChatTools.cs +++ b/src/AI/ChatTools.cs @@ -15,10 +15,6 @@ public static class ChatTools { "type": "object", "properties": { - "repo": { - "type": "string", - "description": "The path to the repository." - }, "file": { "type": "string", "description": "The path to the file." @@ -28,28 +24,25 @@ public static class ChatTools "description": "The path to the original file when it has been renamed or copied." } }, - "required": ["repo", "file"] + "required": ["file"] } """)), false); - public static async Task ProcessAsync(ChatToolCall call, Action output) + 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 hasRepo = doc.RootElement.TryGetProperty("repo", out var repoPath); var hasFile = doc.RootElement.TryGetProperty("file", out var filePath); var hasOriginalFile = doc.RootElement.TryGetProperty("originalFile", out var originalFilePath); - if (!hasRepo) - throw new ArgumentException("repo", "The repo argument is required"); 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(repoPath.GetString(), filePath.GetString(), orgFilePath).ReadAsync(); + 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); } diff --git a/src/AI/ExtraHeadersPolicy.cs b/src/AI/ExtraHeadersPolicy.cs new file mode 100644 index 0000000000..a14924ba64 --- /dev/null +++ b/src/AI/ExtraHeadersPolicy.cs @@ -0,0 +1,48 @@ +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.AI +{ + public class ExtraHeadersPolicy : PipelinePolicy + { + public ExtraHeadersPolicy(string headers) + { + _headers = headers; + } + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + Apply(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + Apply(message); + await ProcessNextAsync(message, pipeline, currentIndex); + } + + private void Apply(PipelineMessage message) + { + var lines = _headers.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var line in lines) + { + var idx = line.IndexOf(':'); + if (idx < 0) + continue; + + var key = line.Substring(0, idx).Trim(); + var value = line.Substring(idx + 1).Trim(); + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) + continue; + + message.Request.Headers.Add(key, value); + } + } + + private readonly string _headers; + } +} diff --git a/src/AI/Options.cs b/src/AI/Options.cs new file mode 100644 index 0000000000..e83b72caad --- /dev/null +++ b/src/AI/Options.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace SourceGit.AI +{ + public static class Options + { + public static readonly string IgnoredReasoningEffortLevel = "unspecified"; + public static readonly List ReasoningEffortLevels = [ + IgnoredReasoningEffortLevel, + "none", + "minimal", + "low", + "medium", + "high", + ]; + } +} diff --git a/src/AI/Service.cs b/src/AI/Service.cs index ebf9170ef6..abc1c63a12 100644 --- a/src/AI/Service.cs +++ b/src/AI/Service.cs @@ -1,7 +1,9 @@ using System; using System.ClientModel; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Text.Json.Serialization; + using Azure.AI.OpenAI; using CommunityToolkit.Mvvm.ComponentModel; using OpenAI; @@ -35,12 +37,6 @@ public bool ReadApiKeyFromEnv set; } = false; - public string AdditionalPrompt - { - get; - set; - } = string.Empty; - [JsonIgnore] public List AvailableModels { @@ -49,6 +45,30 @@ public List AvailableModels } = []; public string Model + { + get => _model; + set => SetProperty(ref _model, value); + } + + public bool AutoFetchAvailableModels + { + get => _autoFetchAvailableModels; + set => SetProperty(ref _autoFetchAvailableModels, value); + } + + public string ReasoningEffortLevel + { + get => _reasoningEffortLevel; + set => SetProperty(ref _reasoningEffortLevel, value); + } + + public string AdditionalPrompt + { + get; + set; + } = string.Empty; + + public string ExtraHeaders { get; set; @@ -56,20 +76,20 @@ public string Model 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) - { - if (string.IsNullOrEmpty(Model) || !AvailableModels.Contains(Model)) - Model = AvailableModels[0]; - } - else - { - Model = null; - } + if (AvailableModels.Count > 0 && (string.IsNullOrEmpty(Model) || !AvailableModels.Contains(Model))) + Model = AvailableModels[0]; } public ChatClient GetChatClient() @@ -80,11 +100,26 @@ public ChatClient GetChatClient() 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) }); + + if (Server.Contains("openai.azure.com/", StringComparison.Ordinal)) + { + if (string.IsNullOrEmpty(ExtraHeaders)) + return new AzureOpenAIClient(new Uri(Server), credential); + + var azureOptions = new AzureOpenAIClientOptions(); + azureOptions.AddPolicy(new ExtraHeadersPolicy(ExtraHeaders), PipelinePosition.PerCall); + return new AzureOpenAIClient(new Uri(Server), credential, azureOptions); + } + + var options = new OpenAIClientOptions() { Endpoint = new Uri(Server) }; + if (!string.IsNullOrEmpty(ExtraHeaders)) + options.AddPolicy(new ExtraHeadersPolicy(ExtraHeaders), PipelinePosition.PerCall); + return new OpenAIClient(credential, options); } private string _name = string.Empty; + private string _model = string.Empty; + private string _reasoningEffortLevel = Options.IgnoredReasoningEffortLevel; + private bool _autoFetchAvailableModels = true; } } diff --git a/src/App.Commands.cs b/src/App.Commands.cs index daa930b763..ad92a5755f 100644 --- a/src/App.Commands.cs +++ b/src/App.Commands.cs @@ -55,9 +55,14 @@ public static bool IsCheckForUpdateCommandVisible } }); + public static readonly Command OpenAppConfigDirCommand = new Command(_ => + { + Native.OS.OpenInFileManager(Native.OS.BasicDirectories.ConfigDir); + }); + public static readonly Command OpenAppDataDirCommand = new Command(_ => { - Native.OS.OpenInFileManager(Native.OS.DataDir); + Native.OS.OpenInFileManager(Native.OS.BasicDirectories.CacheDir); }); public static readonly Command OpenAboutCommand = new Command(async _ => @@ -81,17 +86,30 @@ public static bool IsCheckForUpdateCommandVisible public static readonly Command HideAppCommand = new Command(_ => { - Native.OS.HideSelf(); + if (OperatingSystem.IsMacOS()) + Native.MacOSUtilities.HideSelf(); }); public static readonly Command HideOtherApplicationsCommand = new Command(_ => { - Native.OS.HideOtherApplications(); + if (OperatingSystem.IsMacOS()) + Native.MacOSUtilities.HideOtherApplications(); }); public static readonly Command ShowAllApplicationsCommand = new Command(_ => { - Native.OS.ShowAllApplications(); + if (OperatingSystem.IsMacOS()) + Native.MacOSUtilities.ShowAllApplications(); + }); + + public static readonly Command OpenSSHKeyHelperCommand = new Command(async _ => + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + var vm = new ViewModels.SSHKeyHelper(); + var dialog = new Views.SSHKeyHelper() { DataContext = vm }; + await dialog.ShowDialog(owner); + } }); } } diff --git a/src/App.Extensions.cs b/src/App.Extensions.cs index 7c2d0195a0..08c1b54783 100644 --- a/src/App.Extensions.cs +++ b/src/App.Extensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using Avalonia.Media; namespace SourceGit @@ -18,6 +19,18 @@ 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)) @@ -67,6 +80,12 @@ 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 diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 43dcdde148..8eef67ae72 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -51,20 +51,6 @@ 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, @@ -73,7 +59,6 @@ public override void Write(Utf8JsonWriter writer, DataGridLength value, JsonSeri typeof(DateTimeConverter), typeof(ColorConverter), typeof(GridLengthConverter), - typeof(DataGridLengthConverter), ] )] [JsonSerializable(typeof(Models.ExternalToolCustomization))] @@ -87,5 +72,6 @@ public override void Write(Utf8JsonWriter writer, DataGridLength value, JsonSeri [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 2fa982635c..2663e44ad4 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -8,12 +8,15 @@ + + + @@ -42,6 +45,7 @@ + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 23fc0dcde0..66882b4869 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Fonts; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; @@ -22,7 +23,7 @@ public partial class App : Application [STAThread] public static void Main(string[] args) { - Native.OS.SetupDataDir(); + Native.OS.SetupBasicDirectories(); AppDomain.CurrentDomain.UnhandledException += (_, e) => { @@ -101,8 +102,11 @@ public static async Task AskConfirmAsync(string message, Models.ConfirmBut 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; @@ -187,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)); } } @@ -282,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; @@ -311,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; @@ -337,27 +341,45 @@ private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCo private bool TryLaunchAsFileHistoryViewer(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("--history", StringComparison.Ordinal)) return false; - var file = Path.GetFullPath(args[1]); - var dir = Path.GetDirectoryName(file); + 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"); + Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim()); desktop.Shutdown(-1); return true; } + Native.OS.SetupExternalTools(); + Models.AvatarManager.Instance.Start(); + var repo = test.StdOut.Trim(); - var relFile = Path.GetRelativePath(repo, file); - var viewer = new Views.FileHistories() + var relativePath = Path.GetRelativePath(repo, fullPath).Replace('\\', '/'); + if (File.Exists(fullPath)) { - DataContext = new ViewModels.FileHistories(repo, relFile) - }; - desktop.MainWindow = viewer; + desktop.MainWindow = new Views.FileHistories() + { + 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; } @@ -367,13 +389,13 @@ private bool TryLaunchAsBlameViewer(IClassicDesktopStyleApplicationLifetime desk 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(); if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut)) { - Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository"); + Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim()); desktop.Shutdown(-1); return true; } @@ -402,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); @@ -438,12 +460,10 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) _ipcChannel = new Models.IpcChannel(); if (!_ipcChannel.IsFirstInstance) { - var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty; + var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0] : string.Empty; if (!string.IsNullOrEmpty(arg)) { - if (arg.StartsWith('"') && arg.EndsWith('"')) - arg = arg.Substring(1, arg.Length - 2).Trim(); - + arg = arg.Replace('\\', '/').TrimEnd('/').Trim('\"').Trim(); if (arg.Length > 0 && !Path.IsPathFullyQualified(arg)) arg = Path.GetFullPath(arg); } @@ -456,9 +476,22 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) 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(); @@ -504,6 +537,9 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) #region Check for Updates private void Check4Update(bool manually = false) { + if (_launcher != null) + _launcher.NewVersion = null; + Task.Run(async () => { try @@ -517,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) { diff --git a/src/Commands/AddSafeDirectory.cs b/src/Commands/AddSafeDirectory.cs new file mode 100644 index 0000000000..52e190db69 --- /dev/null +++ b/src/Commands/AddSafeDirectory.cs @@ -0,0 +1,11 @@ +namespace SourceGit.Commands +{ + public class AddSafeDirectory : Command + { + public AddSafeDirectory(string ctx, string value) + { + Context = ctx; + Args = $"config --global --add safe.directory {value.Quoted()}"; + } + } +} diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 0f247d736f..a0bc26ee4c 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -37,7 +37,7 @@ public Blame(string repo, string file, string revision, bool ignoreWhitespace) ParseLine(line); if (_result.IsBinary) - break; + return _result; } if (_needUnifyCommitSHA) @@ -49,6 +49,9 @@ public Blame(string repo, string file, string revision, bool ignoreWhitespace) } } + if (_content.Length > 0) + _content.Length--; // Remove the last newline character + _result.Content = _content.ToString(); return _result; } @@ -66,7 +69,8 @@ private void ParseLine(string line) if (!match.Success) return; - _content.AppendLine(match.Groups[5].Value); + // Force using `\n` as line-ending + _content.Append(match.Groups[5].Value).Append('\n'); var commit = match.Groups[1].Value; var file = match.Groups[2].Value.Trim(); diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index efb325d066..6ed017841e 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/Command.cs b/src/Commands/Command.cs index 2c453b9a1f..14430805c2 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(); + Native.OS.TerminateProcess(captured.Process); } }); } @@ -158,8 +158,36 @@ protected async Task ReadToEndAsync() protected ProcessStartInfo CreateGitStartInfo(bool redirect) { + var useSetSid = CancellationToken.CanBeCanceled && Native.OS.SupportSetSid(); + var selfExecFile = Environment.ProcessPath; + var builder = new StringBuilder(2048); + + if (useSetSid) + builder.Append(Native.OS.GitExecutable.Quoted()).Append(' '); + + builder + .Append("--no-pager -c core.quotepath=off -c credential.helper=") + .Append(Native.OS.CredentialHelper) + .Append(' '); + + switch (Editor) + { + case EditorType.CoreEditor: + builder.Append($"""-c core.editor="\"{selfExecFile}\" --core-editor" """); + break; + case EditorType.RebaseEditor: + builder.Append($"""-c core.editor="\"{selfExecFile}\" --rebase-message-editor" -c sequence.editor="\"{selfExecFile}\" --rebase-todo-editor" -c rebase.abbreviateCommands=true """); + break; + default: + builder.Append("-c core.editor=true "); + break; + } + + builder.Append(Args); + var start = new ProcessStartInfo(); - start.FileName = Native.OS.GitExecutable; + start.FileName = useSetSid ? Native.OS.GetSetSidExecutable() : Native.OS.GitExecutable; + start.Arguments = builder.ToString(); start.UseShellExecute = false; start.CreateNoWindow = true; @@ -172,7 +200,6 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect) } // Force using this app as SSH askpass program - var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec` start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer"); start.Environment.Add("SOURCEGIT_LAUNCH_AS_ASKPASS", "TRUE"); @@ -181,37 +208,15 @@ 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}' -F '/dev/null'"); + start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}' -o AddKeysToAgent=yes"); - // Force using en_US.UTF-8 locale - if (OperatingSystem.IsLinux()) + // Force the C locale on Unix, so that git's output (including fatal errors) can always be parsed. + if (!OperatingSystem.IsWindows()) { start.Environment.Add("LANG", "C"); start.Environment.Add("LC_ALL", "C"); } - var builder = new StringBuilder(2048); - builder - .Append("--no-pager -c core.quotepath=off -c credential.helper=") - .Append(Native.OS.CredentialHelper) - .Append(' '); - - switch (Editor) - { - case EditorType.CoreEditor: - builder.Append($"""-c core.editor="\"{selfExecFile}\" --core-editor" """); - break; - case EditorType.RebaseEditor: - builder.Append($"""-c core.editor="\"{selfExecFile}\" --rebase-message-editor" -c sequence.editor="\"{selfExecFile}\" --rebase-todo-editor" -c rebase.abbreviateCommands=true """); - break; - default: - builder.Append("-c core.editor=true "); - break; - } - - builder.Append(Args); - start.Arguments = builder.ToString(); - // Working directory if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory; diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index 2e1062cfb4..c7ee58915a 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -7,7 +7,7 @@ namespace SourceGit.Commands { public partial class CompareRevisions : Command { - [GeneratedRegex(@"^([MAD])\s+(.+)$")] + [GeneratedRegex(@"^([MADT])\s+(.+)$")] private static partial Regex REG_FORMAT(); [GeneratedRegex(@"^([CR])[0-9]{0,4}\s+(.+)$")] private static partial Regex REG_RENAME_FORMAT(); @@ -73,6 +73,10 @@ public CompareRevisions(string repo, string start, string end, string path) change.Set(Models.ChangeState.Deleted); changes.Add(change); break; + case 'T': + change.Set(Models.ChangeState.TypeChanged); + changes.Add(change); + break; } } diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index 75f4d738a2..78baee3cdf 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; namespace SourceGit.Commands @@ -46,7 +45,7 @@ public async Task> ReadAllAsync() Args = "config -l"; var output = await ReadToEndAsync().ConfigureAwait(false); - var rs = new Dictionary(); + var rs = new Dictionary(StringComparer.OrdinalIgnoreCase); if (output.IsSuccess) { var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); @@ -55,7 +54,7 @@ public async Task> ReadAllAsync() var parts = line.Split('=', 2); if (parts.Length == 2) { - var key = parts[0].ToLower(CultureInfo.CurrentCulture); // Always use lower case for key + var key = parts[0]; var value = parts[1]; rs[key] = value; } diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 4840d14e69..fb008aeee9 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -16,11 +16,29 @@ public partial class Diff : Command [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 "; + + private const int MAX_INLINE_CONTENT_LENGTH = 1024; + private const int MAX_INLINE_CHUNKS_PER_LINE = 16; + + public Diff(string repo, Models.DiffOption opt, int numContextLines, bool ignoreWhitespace, bool ignoreCRAtEOL) { _result.TextDiff = new Models.TextDiff(); @@ -28,12 +46,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(); @@ -50,22 +68,25 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa using var ms = new MemoryStream(); await proc.StandardOutput.BaseStream.CopyToAsync(ms, CancellationToken).ConfigureAwait(false); - var bytes = ms.ToArray(); - var start = 0; - while (start < bytes.Length) + if (ms.TryGetBuffer(out var buffer)) { - var end = Array.IndexOf(bytes, (byte)'\n', start); - if (end < 0) + var start = buffer.Offset; + var end = buffer.Offset + buffer.Count; + while (start < end) { - ParseLine(bytes[start..]); - break; - } + var lineEnd = Array.IndexOf(buffer.Array, (byte)'\n', start); + if (lineEnd < 0) + { + ParseLine(buffer[start..]); + break; + } - ParseLine(bytes[start..end]); - if (_result.IsBinary) - break; + ParseLine(buffer[start..lineEnd]); + if (_result.IsBinary) + break; - start = end + 1; + start = lineEnd + 1; + } } await proc.WaitForExitAsync(CancellationToken).ConfigureAwait(false); @@ -75,171 +96,229 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa // 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(byte[] lineBytes) + private void ParseLine(ArraySegment lineBytes) { - var line = Encoding.UTF8.GetString(lineBytes); - if (ParseFileModeChange(line)) - return; - - if (ParseLFSChange(line)) + // Decode line bytes to UTF-8 string + var line = Encoding.UTF8.GetString(lineBytes.Array, lineBytes.Offset, lineBytes.Count); + if (line.Length == 0) return; - if (_result.TextDiff.Lines.Count == 0) + // 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) { - if (line.StartsWith("Binary", StringComparison.Ordinal)) - { - _result.IsBinary = true; + if (ParseChunkBodyLine(line, lineBytes)) return; - } - if (string.IsNullOrEmpty(_result.OldHash)) - { - var match = REG_HASH_CHANGE().Match(line); - if (!match.Success) - return; + ProcessInlineHighlights(); + _isInChunk = false; + } - _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, lineBytes, 0, 0); - _result.TextDiff.Lines.Add(_last); - } + // If the current line is not a chunk-body, try to parse it as chunk-indicator + if (ParseChunkStartLine(line)) + { + _isInChunk = true; + return; } - else + + // 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) { - if (line.Length == 0) - { - ProcessInlineHighlights(); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", [], _oldLine, _newLine); - _result.TextDiff.Lines.Add(_last); - _oldLine++; - _newLine++; - return; - } + if (string.IsNullOrEmpty(_result.OldHash)) + _result.OldHash = match.Groups[1].Value; + _result.NewHash = match.Groups[2].Value; + return; + } - var ch = line[0]; - if (ch == '-') - { - _result.TextDiff.DeletedLines++; - _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), lineBytes[1..], _oldLine, 0); - _deleted.Add(_last); - _oldLine++; - } - else if (ch == '+') - { - _result.TextDiff.AddedLines++; - _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), lineBytes[1..], 0, _newLine); - _added.Add(_last); - _newLine++; - } - else if (ch != '\\') - { - 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, lineBytes, 0, 0); - _result.TextDiff.Lines.Add(_last); - } - else - { - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), lineBytes[1..], _oldLine, _newLine); - _result.TextDiff.Lines.Add(_last); - _oldLine++; - _newLine++; - } - } - else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal)) - { - _last.NoNewLineEndOfFile = true; - } + if (line.StartsWith(SPECIAL_BINARY, StringComparison.Ordinal)) + _result.IsBinary = true; + } + + private bool ParseChunkStartLine(string line) + { + 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, null, 0, 0); + _result.TextDiff.Lines.Add(_last); + return true; } + + return false; } - private bool ParseFileModeChange(string line) + private bool ParseChunkBodyLine(string line, ArraySegment lineBytes) { - if (line.StartsWith("old mode ", StringComparison.Ordinal)) + var prefix = line[0]; + var content = line.Substring(1); + var rawContent = lineBytes[1..].ToArray(); + if (ParseLFSChange(prefix, content)) + return true; + + if (prefix == PREFIX_DELETED) { - _result.OldMode = line.Substring(9); + _result.TextDiff.DeletedLines++; + _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, content, rawContent, _oldLine, 0); + _deleted.Add(_last); + _oldLine++; return true; } - if (line.StartsWith("new mode ", StringComparison.Ordinal)) + if (prefix == PREFIX_ADDED) { - _result.NewMode = line.Substring(9); + _result.TextDiff.AddedLines++; + _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, content, rawContent, 0, _newLine); + _added.Add(_last); + _newLine++; return true; } - if (line.StartsWith("deleted file mode ", StringComparison.Ordinal)) + if (prefix == PREFIX_CONTEXT) { - _result.OldMode = line.Substring(18); + ProcessInlineHighlights(); + + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, content, rawContent, _oldLine, _newLine); + _result.TextDiff.Lines.Add(_last); + _oldLine++; + _newLine++; return true; } - if (line.StartsWith("new file mode ", StringComparison.Ordinal)) + if (prefix == PREFIX_COMMAND) { - _result.NewMode = line.Substring(14); + if (content.Equals(SPECIAL_NO_NEWLINE, StringComparison.Ordinal)) + _last.NoNewLineEndOfFile = true; return true; } return false; } - private bool ParseLFSChange(string line) + private bool ParseFileModeChange(string line) { - if (_result.IsLFS) + if (line.StartsWith(FILE_MODE_OLD, StringComparison.Ordinal)) { - 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 (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)); + _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 (_result.TextDiff.Lines.Count != 1) - return false; + if (line.StartsWith(FILE_MODE_DELETED, StringComparison.Ordinal)) + { + _result.OldMode = int.Parse(line.AsSpan(18)); + return true; + } - var isLFS = (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal)) || - (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal)) || - (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal)); + if (line.StartsWith(FILE_MODE_ADDED, StringComparison.Ordinal)) + { + _result.NewMode = int.Parse(line.AsSpan(14)); + return true; + } - if (isLFS) + return false; + } + + private bool ParseLFSChange(char prefix, string content) + { + if (_isLFS) { - _result.IsLFS = true; - _result.LFSDiff = new Models.LFSDiff(); + 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 (prefix == PREFIX_ADDED) + { + 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 (prefix == PREFIX_CONTEXT) + { + if (content.StartsWith(LFS_SIZE_PREFIX, StringComparison.Ordinal)) + _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(content.AsSpan(5)); + } 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)) + { + _isLFS = true; + _result.LFSDiff = new Models.LFSDiff(); + return true; + } + } + return false; } @@ -254,11 +333,11 @@ private void ProcessInlineHighlights() var left = _deleted[i]; var right = _added[i]; - if (left.Content.Length > 1024 || right.Content.Length > 1024) + if (left.Content.Length > MAX_INLINE_CONTENT_LENGTH || right.Content.Length > MAX_INLINE_CONTENT_LENGTH) continue; var chunks = Models.TextInlineChange.Compare(left.Content, right.Content); - if (chunks.Count > 4) + if (chunks.Count > MAX_INLINE_CHUNKS_PER_LINE) continue; foreach (var chunk in chunks) @@ -289,5 +368,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/DoesBranchExistOnRemote.cs b/src/Commands/DoesBranchExistOnRemote.cs new file mode 100644 index 0000000000..3a971c3e8f --- /dev/null +++ b/src/Commands/DoesBranchExistOnRemote.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class DoesBranchExistOnRemote : Command + { + public DoesBranchExistOnRemote(string repo, Models.Remote remote, Models.Branch branch) + { + WorkingDirectory = repo; + SSHKey = remote.PrivateSSHKey; + RaiseError = false; + Args = $"ls-remote {remote.Name} {branch.Name}"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync(); + return rs.IsSuccess && rs.StdOut.Trim().Length > 0; + } + } +} diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 9143612576..679ffc9ac6 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -1,53 +1,40 @@ using System.Text; -using System.Threading.Tasks; namespace SourceGit.Commands { public class Fetch : Command { - public Fetch(string repo, string remote, bool noTags, bool force) + public Fetch(string repo, Models.Remote remote, bool noTags, bool force) { - _remote = remote; - WorkingDirectory = repo; Context = repo; + SSHKey = remote.PrivateSSHKey; var builder = new StringBuilder(512); builder.Append("fetch --progress --verbose "); builder.Append(noTags ? "--no-tags " : "--tags "); if (force) builder.Append("--force "); - builder.Append(remote); + builder.Append(remote.Name); Args = builder.ToString(); } - public Fetch(string repo, string remote) + public Fetch(string repo, Models.Remote remote) { - _remote = remote; - WorkingDirectory = repo; Context = repo; + SSHKey = remote.PrivateSSHKey; RaiseError = false; - - Args = $"fetch --progress --verbose {remote}"; + Args = $"fetch --progress --verbose {remote.Name}"; } - public Fetch(string repo, Models.Branch local, Models.Branch remote) + public Fetch(string repo, Models.Remote remote, Models.Branch remoteBranch, Models.Branch local) { - _remote = remote.Remote; - WorkingDirectory = repo; Context = repo; - Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}"; + SSHKey = remote.PrivateSSHKey; + Args = $"fetch --progress --verbose {remote.Name} {remoteBranch.Name}:{local.Name}"; } - - public async Task RunAsync() - { - SSHKey = await new Config(WorkingDirectory).GetAsync($"remote.{_remote}.sshkey").ConfigureAwait(false); - return await ExecAsync().ConfigureAwait(false); - } - - private readonly string _remote; } } diff --git a/src/Commands/GetFileChangeForAI.cs b/src/Commands/GetFileChangeForAI.cs index 96ef162dcb..128d0aba64 100644 --- a/src/Commands/GetFileChangeForAI.cs +++ b/src/Commands/GetFileChangeForAI.cs @@ -6,13 +6,16 @@ namespace SourceGit.Commands { public class GetFileChangeForAI : Command { - public GetFileChangeForAI(string repo, string file, string originalFile) + 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 -- "); + 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()); diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 8e29e6b50e..0438d76de8 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: - Models.Notification.Send(repo, "Bad git-flow branch type!!!", true); + 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 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,22 +93,21 @@ public static async Task FinishAsync(string repo, Models.GitFlowBranchType builder.Append("hotfix"); break; default: - Models.Notification.Send(repo, "Bad git-flow branch type!!!", true); + RaiseException("Bad git-flow branch type!!!"); return false; } builder.Append(" finish "); + if (rebase) + builder.Append("--rebase "); if (squash) builder.Append("--squash "); 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 7e4ca86b2c..6b61b9d2ab 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/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs deleted file mode 100644 index e5b752d354..0000000000 --- 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 bd11b5779a..bcb8811f9a 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 0000000000..2e349a84ba --- /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/MergeTree.cs b/src/Commands/MergeTree.cs new file mode 100644 index 0000000000..41e9ea28aa --- /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 e121445bac..7e8f4f731c 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -1,32 +1,26 @@ using System.Text; -using System.Threading.Tasks; namespace SourceGit.Commands { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase) + public Pull(string repo, Models.Remote remote, Models.Branch remoteBranch, bool useRebase) { - _remote = remote; - WorkingDirectory = repo; Context = repo; + SSHKey = remote.PrivateSSHKey; 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.Name); - Args = builder.ToString(); - } + if (remoteBranch != null) + builder.Append(' ').Append(remoteBranch.Name); - public async Task RunAsync() - { - SSHKey = await new Config(WorkingDirectory).GetAsync($"remote.{_remote}.sshkey").ConfigureAwait(false); - return await ExecAsync().ConfigureAwait(false); + Args = builder.ToString(); } - - private readonly string _remote; } } diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index 44394dc412..e04e81ef94 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -1,16 +1,14 @@ using System.Text; -using System.Threading.Tasks; namespace SourceGit.Commands { public class Push : Command { - public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force) + public Push(string repo, Models.Branch local, Models.Remote remote, Models.Branch remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force, bool noVerify) { - _remote = remote; - WorkingDirectory = repo; Context = repo; + SSHKey = remote.PrivateSSHKey; var builder = new StringBuilder(1024); builder.Append("push --progress --verbose "); @@ -22,33 +20,41 @@ public Push(string repo, string local, string remote, string remoteBranch, bool builder.Append("-u "); if (force) builder.Append("--force-with-lease "); + if (noVerify) + builder.Append("--no-verify "); - builder.Append(remote).Append(' ').Append(local).Append(':').Append(remoteBranch); + builder.Append(remote.Name).Append(' ').Append(local.Name).Append(':').Append(remoteBranch.Name); Args = builder.ToString(); } - public Push(string repo, string remote, string refname, bool isDelete) + public Push(string repo, Models.Commit revision, Models.Remote remote, Models.Branch remoteBranch, bool force) { - _remote = remote; + WorkingDirectory = repo; + Context = repo; + SSHKey = remote.PrivateSSHKey; + + var builder = new StringBuilder(1024); + builder.Append("push --progress --verbose "); + if (force) + builder.Append("--force-with-lease "); + builder.Append(remote.Name).Append(' ').Append(revision.SHA).Append(':').Append(remoteBranch.Name); + Args = builder.ToString(); + } + + public Push(string repo, Models.Remote remote, string refname, bool isDelete) + { WorkingDirectory = repo; Context = repo; + SSHKey = remote.PrivateSSHKey; var builder = new StringBuilder(512); builder.Append("push "); if (isDelete) builder.Append("--delete "); - builder.Append(remote).Append(' ').Append(refname); + builder.Append(remote.Name).Append(' ').Append(refname); Args = builder.ToString(); } - - public async Task RunAsync() - { - SSHKey = await new Config(WorkingDirectory).GetAsync($"remote.{_remote}.sshkey").ConfigureAwait(false); - return await ExecAsync().ConfigureAwait(false); - } - - private readonly string _remote; } } diff --git a/src/Commands/QueryAuthors.cs b/src/Commands/QueryAuthors.cs new file mode 100644 index 0000000000..d5e443d5e4 --- /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 7e7e888773..0000000000 --- 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, _commit.Length)); - } - } - - return outs; - } - - private string _commit; - } -} diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 0005abd6e5..1a5073988d 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); diff --git a/src/Commands/QueryConflictFileState.cs b/src/Commands/QueryConflictFileState.cs new file mode 100644 index 0000000000..f9a52ee121 --- /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 a795e1f548..393160602b 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/QueryFileHistory.cs b/src/Commands/QueryFileHistory.cs index d112377aa0..5dca109a2c 100644 --- a/src/Commands/QueryFileHistory.cs +++ b/src/Commands/QueryFileHistory.cs @@ -8,7 +8,7 @@ namespace SourceGit.Commands { public partial class QueryFileHistory : Command { - [GeneratedRegex(@"^([MAD])\s+(.+)$")] + [GeneratedRegex(@"^([MADT])\s+(.+)$")] private static partial Regex REG_FORMAT(); [GeneratedRegex(@"^([CR])[0-9]{0,4}\s+(.+)$")] private static partial Regex REG_RENAME_FORMAT(); @@ -86,6 +86,9 @@ public QueryFileHistory(string repo, string path, string head) case 'D': last.Change.Set(Models.ChangeState.Deleted); break; + case 'T': + last.Change.Set(Models.ChangeState.TypeChanged); + break; } } } diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs index c1ad51dfe5..c2afe26504 100644 --- a/src/Commands/QueryFileSize.cs +++ b/src/Commands/QueryFileSize.cs @@ -1,29 +1,21 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace SourceGit.Commands { - public partial class QueryFileSize : Command + public class QueryFileSize : Command { - [GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")] - private static partial Regex REG_FORMAT(); - public QueryFileSize(string repo, string file, string revision) { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree {revision} -l -- {file.Quoted()}"; + Args = $"cat-file -s {revision}:{file.Quoted()}"; } public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); - if (rs.IsSuccess) - { - var match = REG_FORMAT().Match(rs.StdOut); - if (match.Success) - return long.Parse(match.Groups[1].Value); - } + if (rs.IsSuccess && long.TryParse(rs.StdOut.Trim(), out var size)) + return size; return 0; } diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index d1bd30a2c8..7c2e4156ab 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -34,9 +34,24 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio try { using var proc = new Process(); + var captured = new CapturedProcess() { Process = proc }; + var capturedLock = new object(); + proc.StartInfo = CreateGitStartInfo(true); proc.Start(); + if (CancellationToken.CanBeCanceled) + { + CancellationToken.Register(() => + { + lock (capturedLock) + { + if (captured is { Process: { HasExited: false } proc }) + Native.OS.TerminateProcess(proc); + } + }); + } + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) { var match = REG_FORMAT().Match(line); @@ -165,6 +180,11 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None) outs.Add(change); } + + lock (capturedLock) + { + captured.Process = null; + } } catch { @@ -173,5 +193,10 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio return outs; } + + private class CapturedProcess + { + public Process Process { get; set; } = null; + } } } diff --git a/src/Commands/QueryPickableCommits.cs b/src/Commands/QueryPickableCommits.cs new file mode 100644 index 0000000000..7ea26e5b2c --- /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 f8a23b7aab..6adff9b36d 100644 --- a/src/Commands/QueryRemotes.cs +++ b/src/Commands/QueryRemotes.cs @@ -1,73 +1,78 @@ using System; using System.Collections.Generic; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace SourceGit.Commands { - public partial class QueryRemotes : Command + public class QueryRemotes { - [GeneratedRegex(@"^([\w\.\-]+)\s*(\S+).*$")] - private static partial Regex REG_REMOTE(); - public QueryRemotes(string repo) { - WorkingDirectory = repo; - Context = repo; - Args = "remote -v"; + _repo = repo; } public async Task> GetResultAsync() { - var outs = new List(); - var rs = await ReadToEndAsync().ConfigureAwait(false); - if (!rs.IsSuccess) - return outs; - - var config = await new Config(WorkingDirectory).ReadAllAsync().ConfigureAwait(false); + var names = new HashSet(); + var urls = new Dictionary(); + var pushUrls = new Dictionary(); + var privateSSHKeys = new Dictionary(); var disableAutoFetchRemotes = new HashSet(); + + var config = await new Config(_repo).ReadAllAsync().ConfigureAwait(false); foreach (var (k, v) in config) { - if (k.StartsWith("remote.", StringComparison.Ordinal) && - k.EndsWith(".disableautofetch", StringComparison.Ordinal) && - v.Equals("true", StringComparison.OrdinalIgnoreCase)) + if (!k.StartsWith("remote.", StringComparison.Ordinal)) + continue; + + if (k.EndsWith(".url", StringComparison.Ordinal)) + { + var name = k.Substring(7, k.Length - 11).Trim('"'); + names.Add(name); + urls[name] = v; + } + else if (k.EndsWith(".pushurl", StringComparison.OrdinalIgnoreCase)) + { + var name = k.Substring(7, k.Length - 15).Trim('"'); + names.Add(name); + pushUrls[name] = v; + } + else if (k.EndsWith(".sshkey", StringComparison.OrdinalIgnoreCase)) { - var remoteName = k.Substring(7, k.Length - 24).Trim('"'); - disableAutoFetchRemotes.Add(remoteName); + var name = k.Substring(7, k.Length - 14).Trim('"'); + names.Add(name); + privateSSHKeys[name] = v; + } + else if (k.EndsWith(".disableautofetch", StringComparison.OrdinalIgnoreCase) && + v.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + var name = k.Substring(7, k.Length - 24).Trim('"'); + disableAutoFetchRemotes.Add(name); } } - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) + var remotes = new List(); + foreach (var name in names) { - var match = REG_REMOTE().Match(line); - if (!match.Success) + if (!urls.TryGetValue(name, out var url)) continue; - var remote = new Models.Remote() + var r = new Models.Remote() { - Name = match.Groups[1].Value, - URL = match.Groups[2].Value, - DisableAutoFetch = disableAutoFetchRemotes.Contains(match.Groups[1].Value) + Name = name, + URL = url, + PushURL = pushUrls.TryGetValue(name, out var pushURL) ? pushURL : null, + PrivateSSHKey = privateSSHKeys.TryGetValue(name, out var privateSSHKey) ? privateSSHKey : null, + DisableAutoFetch = disableAutoFetchRemotes.Contains(name) }; - 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); + remotes.Add(r); } - return outs; + remotes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + return remotes; } + + private readonly string _repo; } } diff --git a/src/Commands/QueryRepositoryStatus.cs b/src/Commands/QueryRepositoryStatus.cs index 3d6adac8e1..8244afb215 100644 --- a/src/Commands/QueryRepositoryStatus.cs +++ b/src/Commands/QueryRepositoryStatus.cs @@ -17,7 +17,7 @@ public QueryRepositoryStatus(string repo) public async Task GetResultAsync() { - Args = "status --porcelain=v2 -b"; + Args = "status --porcelain=v2 -b -uall --ignore-submodules=dirty"; var rs = await ReadToEndAsync().ConfigureAwait(false); if (!rs.IsSuccess) return null; @@ -36,12 +36,17 @@ public QueryRepositoryStatus(string repo) else status.CurrentBranch = head; - if (count == 4 && lines[3].StartsWith("# branch.ab ", StringComparison.Ordinal)) - ParseTrackStatus(status, lines[3].Substring(12).Trim()); + if (count >= 4) + { + var changesCount = count - 3; + if (lines[3].StartsWith("# branch.ab ", StringComparison.Ordinal)) + { + ParseTrackStatus(status, lines[3].Substring(12).Trim()); + changesCount--; + } - status.LocalChanges = await new CountLocalChanges(WorkingDirectory, true) { RaiseError = false } - .GetResultAsync() - .ConfigureAwait(false); + status.LocalChanges = changesCount; + } return status; } diff --git a/src/Commands/QueryStagedFileBlobGuid.cs b/src/Commands/QueryStagedFileBlobGuid.cs deleted file mode 100644 index 54ecd2ac76..0000000000 --- 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 0000000000..a61f2d880a --- /dev/null +++ b/src/Commands/QuerySubmoduleRevision.cs @@ -0,0 +1,62 @@ +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 new Models.RevisionSubmodule(_revision); + + var commit = new Models.Commit(); + var lines = rs.StdOut.Split('\0'); + if (lines.Length < 9) + return new Models.RevisionSubmodule(_revision); + + 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 = lines[8].TrimEnd(); + var uncommittedChangesCount = 0; + if (_hasUncommittedChange) + uncommittedChangesCount = await new CountLocalChanges(WorkingDirectory, true) + .GetResultAsync() + .ConfigureAwait(false); + + return new Models.RevisionSubmodule(commit, message, uncommittedChangesCount); + } + + private bool _hasUncommittedChange = false; + private string _revision = string.Empty; + } +} diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 8718542eb3..9033a8d047 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/Rebase.cs b/src/Commands/Rebase.cs index f7f33ac5c0..fdc585c019 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/Remote.cs b/src/Commands/Remote.cs index 6a15037740..f7296b1cef 100644 --- a/src/Commands/Remote.cs +++ b/src/Commands/Remote.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Text; +using System.Threading.Tasks; namespace SourceGit.Commands { @@ -28,33 +29,25 @@ public async Task RenameAsync(string name, string to) return await ExecAsync(); } - public async Task PruneAsync(string name) + public async Task PruneAsync(string name, string sshKey) { + SSHKey = sshKey; Args = $"remote prune {name}"; return await ExecAsync(); } - public async Task GetURLAsync(string name, bool isPush) + public async Task SetURLAsync(string name, string url, bool isDelete, bool isPush) { - Args = "remote get-url" + (isPush ? " --push " : " ") + name; + var builder = new StringBuilder(); + builder.Append("remote set-url "); + if (isDelete) + builder.Append("--delete "); + if (isPush) + builder.Append("--push "); + builder.Append(name).Append(' ').Append(url); - var rs = await ReadToEndAsync(); - return rs.IsSuccess ? rs.StdOut.Trim() : string.Empty; - } - - public async Task SetURLAsync(string name, string url, bool isPush) - { - Args = "remote set-url" + (isPush ? " --push " : " ") + $"{name} {url}"; + Args = builder.ToString(); return await ExecAsync(); } - - public async Task HasBranchAsync(string remote, string branch) - { - SSHKey = await new Config(WorkingDirectory).GetAsync($"remote.{remote}.sshkey"); - Args = $"ls-remote {remote} {branch}"; - - var rs = await ReadToEndAsync(); - return rs.IsSuccess && rs.StdOut.Trim().Length > 0; - } } } diff --git a/src/Commands/Replay.cs b/src/Commands/Replay.cs new file mode 100644 index 0000000000..3f2546224b --- /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/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 410d15b07c..6c810289d1 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -16,7 +16,7 @@ public static async Task RunAsync(string repo, string revision, string file, str var isLFSFiltered = await new IsLFSFiltered(repo, revision, file).GetResultAsync().ConfigureAwait(false); if (isLFSFiltered) { - var pointerStream = await QueryFileContent.RunAsync(repo, revision, file).ConfigureAwait(false); + await using var pointerStream = await QueryFileContent.RunAsync(repo, revision, file).ConfigureAwait(false); await ExecCmdAsync(repo, "lfs smudge", saveTo, pointerStream).ConfigureAwait(false); } else diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 2d43c7629f..3c6ed017b7 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 728296ce30..d8b1dbcb7f 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -43,13 +43,15 @@ public async Task SetBranchAsync(string path, string branch) return await ExecAsync().ConfigureAwait(false); } - public async Task UpdateAsync(List modules, bool init = false, bool useRemote = false) + public async Task UpdateAsync(List modules, bool init, bool recursive, bool useRemote) { var builder = new StringBuilder(); - builder.Append("submodule update --recursive"); + builder.Append("submodule update"); if (init) builder.Append(" --init"); + if (recursive) + builder.Append(" --recursive"); if (useRemote) builder.Append(" --remote"); if (modules.Count > 0) diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index 7b70e2ab49..137bb3f7f1 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; @@ -61,20 +62,33 @@ public Worktree(string repo) return worktrees; } - public async Task AddAsync(string fullpath, string name, bool createNew, string tracking) + public async Task AddAsync(string fullpath, string name) + { + if (string.IsNullOrEmpty(name)) + Args = $"worktree add {fullpath.Quoted()}"; + else + Args = $"worktree add -b {name} {fullpath.Quoted()}"; + + return await ExecAsync().ConfigureAwait(false); + } + + public async Task AddWithLocalBranchAsync(string fullpath, string localBranch) + { + Args = $"worktree add -B {localBranch} {fullpath.Quoted()} {localBranch}"; + return await ExecAsync().ConfigureAwait(false); + } + + public async Task AddWithRemoteBranchAsync(string fullpath, string name, string remoteBranch, bool tracking) { var builder = new StringBuilder(1024); builder.Append("worktree add "); - if (!string.IsNullOrEmpty(tracking)) - builder.Append("--track "); - if (!string.IsNullOrEmpty(name)) - builder.Append(createNew ? "-b " : "-B ").Append(name).Append(' '); - builder.Append(fullpath.Quoted()).Append(' '); - - if (!string.IsNullOrEmpty(tracking)) - builder.Append(tracking); - else if (!string.IsNullOrEmpty(name) && !createNew) - builder.Append(name); + + if (tracking) + builder.Append("--track -b ").Append(string.IsNullOrEmpty(name) ? Path.GetFileName(fullpath) : name).Append(' '); + else if (!string.IsNullOrEmpty(name)) + builder.Append("-b ").Append(name).Append(' '); + + builder.Append(fullpath.Quoted()).Append(' ').Append(remoteBranch); Args = builder.ToString(); return await ExecAsync().ConfigureAwait(false); diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 2dcebcf277..4d6c723a8d 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -1,7 +1,5 @@ using Avalonia; -using Avalonia.Controls; using Avalonia.Data.Converters; -using Avalonia.Media; namespace SourceGit.Converters { @@ -16,16 +14,16 @@ public static class IntConverters public static readonly FuncValueConverter IsZero = new(v => v == 0); - public static readonly FuncValueConverter IsNotOne = - new(v => v != 1); - public static readonly FuncValueConverter ToTreeMargin = new(v => new Thickness(v * 16, 0, 0, 0)); - public static readonly FuncValueConverter ToBookmarkBrush = - new(v => Models.Bookmarks.Get(v) ?? Application.Current?.FindResource("Brush.FG1") as IBrush); - public static readonly FuncValueConverter ToUnsolvedDesc = new(v => v == 0 ? App.Text("MergeConflictEditor.AllResolved") : App.Text("MergeConflictEditor.ConflictsRemaining", v)); + + public static readonly FuncValueConverter ToBadgeWithLimit = + new(v => v > 999 ? "999+" : v.ToString()); + + public static readonly FuncValueConverter IsMaxCountOfStatistics = + new(v => v >= 200000); } } diff --git a/src/Converters/LongConverters.cs b/src/Converters/LongConverters.cs index 05a55e95bd..fb72aadb79 100644 --- a/src/Converters/LongConverters.cs +++ b/src/Converters/LongConverters.cs @@ -18,6 +18,11 @@ public static class LongConverters return $"{(bytes / GB):G3} GB ({bytes:N0})"; }); + public static readonly FuncValueConverter IsGreaterThanZero = new(bytes => + { + return bytes > 0; + }); + private const double KB = 1024; private const double MB = 1024 * 1024; private const double GB = 1024 * 1024 * 1024; diff --git a/src/Converters/ObjectConverters.cs b/src/Converters/ObjectConverters.cs deleted file mode 100644 index f7c57764dd..0000000000 --- a/src/Converters/ObjectConverters.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class ObjectConverters - { - public class IsTypeOfConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value == null || parameter == null) - return false; - - return value.GetType().IsAssignableTo((Type)parameter); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return new NotImplementedException(); - } - } - - public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter(); - } -} diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index c8d573a227..7e4593fd86 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -38,13 +38,13 @@ public static AvatarManager Instance private readonly Lock _synclock = new(); private string _storePath; private List _avatars = new List(); - private Dictionary _resources = new Dictionary(); + private Cache _cache = new Cache(256); private HashSet _requesting = new HashSet(); private HashSet _defaultAvatars = new HashSet(); public void Start() { - _storePath = Path.Combine(Native.OS.DataDir, "avatars"); + _storePath = Path.Combine(Native.OS.BasicDirectories.CacheDir, "avatars"); if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath); @@ -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; @@ -88,8 +91,6 @@ public void Start() Bitmap img = null; try { - using var client = new HttpClient(); - client.Timeout = TimeSpan.FromSeconds(2); var rsp = await client.GetAsync(url); if (rsp.IsSuccessStatusCode) { @@ -119,7 +120,7 @@ public void Start() Dispatcher.UIThread.Post(() => { - _resources[email] = img; + _cache.AddOrUpdate(email, img); NotifyResourceChanged(email, img); }); } @@ -145,7 +146,7 @@ public Bitmap Request(string email, bool forceRefetch) if (_defaultAvatars.Contains(email)) return null; - _resources.Remove(email); + _cache.Remove(email); var localFile = Path.Combine(_storePath, GetEmailHash(email)); if (File.Exists(localFile)) @@ -155,7 +156,7 @@ public Bitmap Request(string email, bool forceRefetch) } else { - if (_resources.TryGetValue(email, out var value)) + if (_cache.TryGet(email, out var value)) return value; var localFile = Path.Combine(_storePath, GetEmailHash(email)); @@ -166,7 +167,7 @@ public Bitmap Request(string email, bool forceRefetch) using (var stream = File.OpenRead(localFile)) { var img = Bitmap.DecodeToWidth(stream, 128); - _resources.Add(email, img); + _cache.AddOrUpdate(email, img); return img; } } @@ -196,7 +197,7 @@ public void SetFromLocal(string email, string file) image = Bitmap.DecodeToWidth(stream, 128); } - _resources[email] = image; + _cache.AddOrUpdate(email, image); lock (_synclock) { @@ -216,7 +217,7 @@ public void SetFromLocal(string email, string file) private void LoadDefaultAvatar(string key, string img) { var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute)); - _resources.Add(key, new Bitmap(icon)); + _cache.AddOrUpdate(key, new Bitmap(icon)); _defaultAvatars.Add(key); } @@ -224,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) @@ -235,5 +233,76 @@ private void NotifyResourceChanged(string email, Bitmap image) foreach (var avatar in _avatars) avatar.OnAvatarResourceChanged(email, image); } + + private class Cache + { + private readonly int _capacity; + private readonly LinkedList _imgList; + private readonly Dictionary> _key2img; + + private class Item + { + public string Key { get; } + public Bitmap Image { get; set; } + + public Item(string key, Bitmap image) + { + Key = key; + Image = image; + } + } + + public Cache(int capacity) + { + _capacity = capacity; + _key2img = new Dictionary>(); + _imgList = new LinkedList(); + } + + public bool TryGet(string key, out Bitmap bitmap) + { + if (_key2img.TryGetValue(key, out var node)) + { + _imgList.Remove(node); + _imgList.AddFirst(node); + bitmap = node.Value.Image; + return true; + } + + bitmap = null; + return false; + } + + public void AddOrUpdate(string key, Bitmap bitmap) + { + if (_key2img.TryGetValue(key, out var node)) + { + _imgList.Remove(node); + _imgList.AddFirst(node); + node.Value.Image = bitmap; + return; + } + + if (_key2img.Count >= _capacity) + { + var lastNode = _imgList.Last; + _imgList.RemoveLast(); + _key2img.Remove(lastNode.Value.Key); + } + + var newNode = new LinkedListNode(new Item(key, bitmap)); + _imgList.AddFirst(newNode); + _key2img[key] = newNode; + } + + public void Remove(string key) + { + if (_key2img.TryGetValue(key, out var node)) + { + _imgList.Remove(node); + _key2img.Remove(key); + } + } + } } } diff --git a/src/Models/BinaryFile.cs b/src/Models/BinaryFile.cs new file mode 100644 index 0000000000..d875ac2f5b --- /dev/null +++ b/src/Models/BinaryFile.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; + +namespace SourceGit.Models +{ + public class BinaryFile : IDisposable + { + public long Size + { + get => _size; + } + + public BinaryFile(string file, bool needDeleteFile = false) + { + _filePath = file; + _needDeleteFile = needDeleteFile; + + if (File.Exists(_filePath)) + { + _size = new FileInfo(_filePath).Length; + _reader = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BUFFER_SIZE, FileOptions.RandomAccess); + _readedStart = 0; + _readedEnd = Math.Min(_size, BUFFER_SIZE); + + if (_size > 0) + { + _reader.Seek(_readedStart, SeekOrigin.Begin); + _reader.ReadExactly(_buffer, 0, (int)_readedEnd); + } + } + } + + public void Dispose() + { + _reader?.Dispose(); + _reader = null; + + if (_needDeleteFile && File.Exists(_filePath)) + File.Delete(_filePath); + } + + public ArraySegment Read(long offset, long length) + { + if (_reader == null || _size == 0 || offset >= _size) + return Array.Empty(); + + if (length > 8192) + length = 8192; + + if (offset + length > _size) + length = _size - offset; + + if (_readedStart <= offset && _readedEnd >= offset + length) + return new ArraySegment(_buffer, (int)(offset - _readedStart), (int)length); + + _readedStart = (Math.Max(0, offset - 2048) / 1024) * 1024; + _readedEnd = Math.Min(_readedStart + BUFFER_SIZE, _size); + + _reader.Seek(_readedStart, SeekOrigin.Begin); + _reader.ReadExactly(_buffer, 0, (int)(_readedEnd - _readedStart)); + + return new ArraySegment(_buffer, (int)(offset - _readedStart), (int)length); + } + + private const int BUFFER_SIZE = 16384; + + private string _filePath = string.Empty; + private bool _needDeleteFile = false; + private FileStream _reader = null; + private long _size = 0; + private long _readedStart = 0; + private long _readedEnd = 0; + private byte[] _buffer = new byte[BUFFER_SIZE]; + } +} diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs index 2ed8beb259..286a02c4f3 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/Commit.cs b/src/Models/Commit.cs index 7f55e31f8f..de299ed6b7 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,13 +8,12 @@ public enum CommitSearchMethod { BySHA = 0, ByAuthor, - ByCommitter, ByMessage, ByPath, ByContent, } - public class Commit + public class Commit : ObservableObject { public string SHA { get; set; } = string.Empty; public User Author { get; set; } = User.Invalid; @@ -28,6 +28,12 @@ public class Commit public int Color { get; set; } = 0; public double LeftMargin { get; set; } = 0; + 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; @@ -118,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 82505c3518..916a79b1bb 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -8,6 +8,15 @@ namespace SourceGit.Models { public record CommitGraphLayout(double StartY, double ClipWidth, double RowHeight); + public enum CommitGraphHighlighting + { + All = 0, + CurrentBranchOnly, + SelectedCommitsOnly, + CurrentBranchAndSelectedCommits, + SelectedCommitsOnlyFirstParent, + } + public class CommitGraph { public static List Pens { get; } = []; @@ -27,11 +36,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 +49,7 @@ public class Link public Point Control; public Point End; public int Color; - public bool IsMerged; + public bool IsHighlighted; } public enum DotType @@ -55,14 +64,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 firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet highlightExtraCommits) { const double unitWidth = 12; const double halfWidth = 6; @@ -74,11 +83,11 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable var ended = new List(); var offsetY = -halfHeight; var colorPicker = new ColorPicker(); + var defHighlighting = highlighting == CommitGraphHighlighting.All; foreach (var commit in commits) { PathHelper major = null; - var isMerged = commit.IsMerged; // Update current y offset offsetY += unitHeight; @@ -86,6 +95,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 = defHighlighting; foreach (var l in unsolved) { if (l.Next.Equals(commit.SHA, StringComparison.Ordinal)) @@ -94,6 +104,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable { offsetX += unitWidth; major = l; + isHighlighted = major.IsHighlighted; if (commit.Parents.Count > 0) { @@ -110,9 +121,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 +141,41 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable } ended.Clear(); + // Calculate highlighted state + if (!isHighlighted) + { + switch (highlighting) + { + case CommitGraphHighlighting.CurrentBranchOnly: + isHighlighted = commit.IsMerged; + break; + case CommitGraphHighlighting.SelectedCommitsOnly: + case CommitGraphHighlighting.SelectedCommitsOnlyFirstParent: + if (highlightExtraCommits.Remove(commit.SHA)) + { + isHighlighted = true; + // Highlight first parent, other parents are dealt with later + if (commit.Parents.Count > 0) + highlightExtraCommits.Add(commit.Parents[0]); + } + break; + default: // CommitGraphHighlighting.CurrentBranchAndSelectedCommits + if (commit.IsMerged) + { + isHighlighted = true; + } + else if (highlightExtraCommits.Remove(commit.SHA)) + { + isHighlighted = true; + // Highlight first parent, other parents are dealt with later + if (commit.Parents.Count > 0) + highlightExtraCommits.Add(commit.Parents[0]); + } + break; + } + } + 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 +184,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) @@ -163,16 +210,19 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable // Deal with other parents (the first parent has been processed) if (!firstParentOnlyEnabled) { + if (highlighting == CommitGraphHighlighting.SelectedCommitsOnlyFirstParent) + isHighlighted = false; + for (int j = 1; j < commit.Parents.Count; j++) { var parentHash = commit.Parents[j]; 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 +232,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 +240,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 +295,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 +394,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/Conflict.cs b/src/Models/Conflict.cs index 112fdd9b75..fd9c690308 100644 --- a/src/Models/Conflict.cs +++ b/src/Models/Conflict.cs @@ -2,6 +2,14 @@ namespace SourceGit.Models { + public enum ConflictFileState + { + Unknown, + UnmergedText, + UnmergedBinary, + Resolved + } + public enum ConflictPanelType { Ours, diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index def59beddd..af64b39a4b 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -6,16 +6,7 @@ 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; @@ -28,7 +19,6 @@ public static bool IgnoreCRAtEOL /// public DiffOption(Change change, bool isUnstaged) { - _workingCopyChange = change; _isUnstaged = isUnstaged; _path = change.Path; _orgPath = change.OriginalPath; @@ -66,18 +56,6 @@ public DiffOption(Commit commit, Change change) _orgPath = change.OriginalPath; } - /// - /// Diff with filepath. Used by FileHistories - /// - /// - /// - public DiffOption(Commit commit, string file) - { - _revisions.Add(commit.FirstParentToCompare); - _revisions.Add(commit.SHA); - _path = file; - } - /// /// Used to diff in `FileHistory` /// @@ -171,7 +149,6 @@ 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; diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 27a8ff093d..35b175c530 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 @@ -34,546 +33,26 @@ public class TextDiffLine public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); public TextDiffLine() { } - public TextDiffLine(TextDiffLineType type, string line, byte[] rawContent, int oldLine, int newLine) + public TextDiffLine(TextDiffLineType type, string content, byte[] rawContent, int oldLine, int newLine) { Type = type; - Content = line; + 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 int AddedLines { get; set; } = 0; public int DeletedLines { get; set; } = 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(string file, 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/{file} b/{file}"); - if (!revert && !isTracked) - writer.WriteLine("new file mode 100644"); - writer.WriteLine($"index 00000000...{fileGuid}"); - writer.WriteLine($"--- {(revert || isTracked ? $"a/{file}" : "/dev/null")}"); - writer.WriteLine($"+++ b/{file}"); - - 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) - WriteLine(writer, '+', line); - else - WriteLine(writer, ' ', line); - } - } - 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; - WriteLine(writer, '+', line); - } - } - - writer.Flush(); - } - - public void GeneratePatchFromSelection(string file, string fileTreeGuid, TextDiffSelection selection, bool revert, string output) - { - using var writer = new StreamWriter(output); - writer.NewLine = "\n"; - writer.WriteLine($"diff --git a/{file} b/{file}"); - writer.WriteLine($"index 00000000...{fileTreeGuid} 100644"); - writer.WriteLine($"--- a/{file}"); - writer.WriteLine($"+++ b/{file}"); - - // If last line of selection is a change. Find one more line. - TextDiffLine 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; - 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); - } - } - - if (tail != null) - WriteLine(writer, ' ', tail); - writer.Flush(); - } - - public void GeneratePatchFromSelectionSingleSide(string file, string fileTreeGuid, TextDiffSelection selection, bool revert, bool isOldSide, string output) - { - using var writer = new StreamWriter(output); - writer.NewLine = "\n"; - writer.WriteLine($"diff --git a/{file} b/{file}"); - writer.WriteLine($"index 00000000...{fileTreeGuid} 100644"); - writer.WriteLine($"--- a/{file}"); - writer.WriteLine($"+++ b/{file}"); - - // If last line of selection is a change. Find one more line. - TextDiffLine 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; - break; - } - } - else - { - if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) - { - tail = line; - 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++; - } - } - } - } - - if (tail != null) - WriteLine(writer, ' ', 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.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"); - } - - [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] - private static partial Regex REG_INDICATOR(); + 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 @@ -584,6 +63,10 @@ public class LFSDiff public class BinaryDiff { + public string Repository { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string NewRevision { get; set; } = string.Empty; + public long OldSize { get; set; } = 0; public long NewSize { get; set; } = 0; } @@ -600,6 +83,12 @@ 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 @@ -616,26 +105,12 @@ public class SubmoduleDiff 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/ExternalTool.cs b/src/Models/ExternalTool.cs index d6d1030a98..04dc2acd4b 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -27,11 +27,13 @@ public LaunchOption(string title, string 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> optionsGenerator = null) + public ExternalTool(string name, string icon, string execFile, Func> optionsGenerator, bool supportOpenFolder) { Name = name; ExecFile = execFile; + SupportOpenFolder = supportOpenFolder; _optionsGenerator = optionsGenerator; @@ -61,6 +63,7 @@ public void Launch(string args) FileName = ExecFile, Arguments = args, UseShellExecute = false, + CreateNoWindow = true, }); } } @@ -82,30 +85,18 @@ 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")] @@ -130,7 +121,7 @@ public List Tools public ExternalToolsFinder() { - var customPathsConfig = Path.Combine(Native.OS.DataDir, "external_editors.json"); + var customPathsConfig = Path.Combine(Native.OS.BasicDirectories.ConfigDir, "external_editors.json"); try { if (File.Exists(customPathsConfig)) @@ -147,20 +138,20 @@ public ExternalToolsFinder() _customization ??= new ExternalToolCustomization(); } - public void TryAdd(string name, string icon, Func finder, Func> optionsGenerator = null) + public void TryAdd(string name, string icon, Func finder, Func> optionsGenerator = null, bool supportOpenFolder = true) { if (_customization.Excludes.Contains(name)) return; if (_customization.Tools.TryGetValue(name, out var customPath) && File.Exists(customPath)) { - Tools.Add(new ExternalTool(name, icon, customPath, optionsGenerator)); + 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, optionsGenerator)); + Tools.Add(new ExternalTool(name, icon, path, optionsGenerator, supportOpenFolder)); } } @@ -196,8 +187,7 @@ 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 { "AI", "CL", "DB", "DL", "DS", "GO", "IC", "IU", "JB", "PC", "PS", "PY", "QA", "QD", "RD", "RM", "RR", "WRS", "WS" }; var state = Path.Combine(platformFinder(), "state.json"); if (File.Exists(state)) { @@ -207,13 +197,15 @@ public void FindJetBrainsFromToolbox(Func platformFinder) var stateData = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.JetBrainsState); foreach (var tool in stateData.Tools) { - if (exclude.Contains(tool.ToolId.ToLowerInvariant())) + if (!supported.Contains(tool.ProductCode)) continue; Tools.Add(new ExternalTool( $"{tool.DisplayName} {tool.DisplayVersion}", - supportedIcons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB", - Path.Combine(tool.InstallLocation, tool.LaunchCommand))); + $"JetBrains/{tool.ProductCode}", + Path.Combine(tool.InstallLocation, tool.LaunchCommand), + null, + true)); } } catch diff --git a/src/Models/GitFlow.cs b/src/Models/GitFlow.cs index 05ade0acee..a648d4db27 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 a23456b678..2f3540e8b7 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 8aae63a3b2..71fb4657ec 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/HistoryFilter.cs b/src/Models/HistoryFilter.cs index b09f074cb2..369b237d72 100644 --- a/src/Models/HistoryFilter.cs +++ b/src/Models/HistoryFilter.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { @@ -38,9 +39,11 @@ public FilterMode Mode set => SetProperty(ref _mode, value); } - public bool IsBranch + [JsonIgnore] + public bool IsValid { - get => Type != FilterType.Tag; + get => _isValid; + set => SetProperty(ref _isValid, value); } public HistoryFilter() @@ -56,5 +59,6 @@ public HistoryFilter(string pattern, FilterType type, FilterMode mode) private string _pattern = string.Empty; private FilterMode _mode = FilterMode.None; + private bool _isValid = true; } } diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs index 702f0630be..1a169d2acf 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; @@ -8,18 +10,56 @@ namespace SourceGit.Models { public class IpcChannel : IDisposable { - public bool IsFirstInstance { get; } + public bool IsFirstInstance { get; private set; } public event Action MessageReceived; public IpcChannel() { - try + IsFirstInstance = false; + + var lockFile = GetLockFilePath(); + _lockFilePath = lockFile.Path; + + if (OperatingSystem.IsLinux() && lockFile.NeedChangePermissions) + { + // On Linux, if the lock file is created in the XDG_RUNTIME_DIR, we need to set the permissions to 700 (rwx------) to ensure that only the current user can access it. + try + { + _singletonLock = new FileStream(_lockFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); + File.SetUnixFileMode(_lockFilePath, File.GetUnixFileMode(_lockFilePath) | UnixFileMode.StickyBit); + IsFirstInstance = true; + } + catch + { + try + { + _singletonLock = new FileStream(_lockFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + IsFirstInstance = true; + } + catch + { + // Just ignore the exception and assume that another instance is running. + } + } + } + else + { + try + { + _singletonLock = File.Open(_lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + IsFirstInstance = true; + } + catch + { + // Just ignore the exception and assume that another instance is running. + } + } + + if (IsFirstInstance) { - _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, @@ -27,17 +67,13 @@ public IpcChannel() _cancellationTokenSource = new CancellationTokenSource(); Task.Run(StartServer); } - catch - { - IsFirstInstance = false; - } } public void SendToFirstInstance(string cmd) { try { - using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) + using (var client = new NamedPipeClientStream(".", GetPipeName(), PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) { client.Connect(1000); if (!client.IsConnected) @@ -65,6 +101,45 @@ public void Dispose() { _cancellationTokenSource?.Cancel(); _singletonLock?.Dispose(); + + if (IsFirstInstance && File.Exists(_lockFilePath)) + File.Delete(_lockFilePath); + } + + private LockFile GetLockFilePath() + { + // On Windows and macOS, we can use the cache directory for the lock file. + if (!OperatingSystem.IsLinux()) + return new LockFile(Path.Combine(Native.OS.BasicDirectories.CacheDir, "process.lock"), false); + + // On Linux, we should first check if the app is running in 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 new LockFile(Path.Combine(portableDir, "process.lock"), false); + } + + // If not in portable mode, we should use the XDG_RUNTIME_DIR environment variable for the lock file. + var runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + if (string.IsNullOrEmpty(runtimeDir) || !Directory.Exists(runtimeDir)) + return new LockFile(Path.Combine(Native.OS.BasicDirectories.CacheDir, "process.lock"), false); + + return new LockFile(Path.Combine(runtimeDir, "sourcegit.instance.lock"), true); + } + + private 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.BasicDirectories.CacheDir.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() @@ -93,6 +168,9 @@ private async void StartServer() } } + private record LockFile(string Path, bool NeedChangePermissions); + + private string _lockFilePath = null; private FileStream _singletonLock = null; private NamedPipeServerStream _server = null; private CancellationTokenSource _cancellationTokenSource = null; diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index 0274333366..5bdb2431bc 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/Null.cs b/src/Models/Null.cs index 1820d4d050..9e25977d07 100644 --- a/src/Models/Null.cs +++ b/src/Models/Null.cs @@ -1,4 +1,7 @@ namespace SourceGit.Models { - public class Null; + public class Null + { + public static readonly Null Instance = new Null(); + } } diff --git a/src/Models/PatchGenerator.cs b/src/Models/PatchGenerator.cs new file mode 100644 index 0000000000..d73b78e1aa --- /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/RefName.cs b/src/Models/RefName.cs new file mode 100644 index 0000000000..42ad6d785d --- /dev/null +++ b/src/Models/RefName.cs @@ -0,0 +1,84 @@ +using System; + +namespace SourceGit.Models +{ + /// + /// Validates branch and tag names using the same rules that git itself enforces, + /// mirroring `git check-ref-format --allow-onelevel`. Rules are taken from the git-check-ref-format documentation. + /// Rule 2 (a refname must contain at least one slash) is intentionally waived, matching `--allow-onelevel`. + /// + public static class RefName + { + public static bool IsValidBranchName(string name) + { + if (string.Equals(name, "HEAD", StringComparison.Ordinal)) + return false; + + return IsValidRefName(name); + } + + public static bool IsValidTagName(string name) => IsValidRefName(name); + + private static bool IsValidRefName(string name) + { + if (string.IsNullOrEmpty(name)) + return false; + + // Anything starting with '-' is treated as a CLI option. + if (name.StartsWith('-')) + return false; + + // Rule 9: cannot be the single character '@'. + if (name.Equals("@", StringComparison.Ordinal)) + return false; + + // Rule 6: cannot begin or end with '/', or contain consecutive slashes. + if (name[0] == '/' || name[^1] == '/' || name.Contains("//", StringComparison.Ordinal)) + return false; + + // Rule 7: cannot end with a dot. + if (name[^1] == '.') + return false; + + // Rule 3: cannot contain two consecutive dots. + if (name.Contains("..", StringComparison.Ordinal)) + return false; + + // Rule 8: cannot contain the sequence '@{'. + if (name.Contains("@{", StringComparison.Ordinal)) + return false; + + // Rules 4, 5 & 10: no control chars, DEL, space, or ~ ^ : ? * [ \. + foreach (var ch in name) + { + if (ch is < ' ' or '\x7f') + return false; + + switch (ch) + { + case ' ': + case '~': + case '^': + case ':': + case '?': + case '*': + case '[': + case '\\': + return false; + } + } + + // Rule 1: no slash-separated component may begin with a dot or end with ".lock". + foreach (var component in name.Split('/')) + { + if (component[0] == '.') + return false; + + if (component.EndsWith(".lock", StringComparison.Ordinal)) + return false; + } + + return true; + } + } +} diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index 12681b4b4d..ab16874d26 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -31,6 +31,8 @@ public partial class Remote public string Name { get; set; } public string URL { get; set; } + public string PushURL { get; set; } + public string PrivateSSHKey { get; set; } public bool DisableAutoFetch { get; set; } public static bool IsSSH(string url) diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 17beedb824..506e6ded78 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -30,6 +30,12 @@ public string ConventionalTypesOverride set; } = string.Empty; + public bool EnableRecursiveWhenAutoUpdatingSubmodules + { + get; + set; + } = true; + public bool AskBeforeAutoUpdatingSubmodules { get; @@ -48,12 +54,6 @@ public AvaloniaList CommitTemplates set; } = []; - public AvaloniaList CommitMessages - { - get; - set; - } = []; - public AvaloniaList CustomActions { get; @@ -116,25 +116,6 @@ public void Save() } } - public void PushCommitMessage(string message) - { - message = message.Trim().ReplaceLineEndings("\n"); - var existIdx = CommitMessages.IndexOf(message); - if (existIdx == 0) - return; - - if (existIdx > 0) - { - CommitMessages.Move(existIdx, 0); - return; - } - - if (CommitMessages.Count > 9) - CommitMessages.RemoveRange(9, CommitMessages.Count - 9); - - CommitMessages.Insert(0, message); - } - public CustomAction AddNewCustomAction() { var act = new CustomAction() { Name = "Unnamed Action" }; @@ -165,10 +146,7 @@ public void MoveCustomActionDown(CustomAction act) private static string HashContent(string source) { var hash = MD5.HashData(Encoding.Default.GetBytes(source)); - 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 static Dictionary _cache = new(); diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index 53fbd801b5..0b35f8d618 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -27,23 +27,35 @@ public bool IsSHAColumnVisibleInHistory set; } = true; - public bool IsDateTimeColumnVisibleInHistory + 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 bool OnlyHighlightCurrentBranchInHistory + public CommitGraphHighlighting GraphHighlighting { get; set; - } = false; + } = CommitGraphHighlighting.All; public BranchSortMode LocalBranchSortMode { @@ -219,6 +231,18 @@ public AvaloniaList HistoryFilters 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")); @@ -352,7 +376,7 @@ public void RenameBranchFilter(string oldName, string newName) if (filter.Type == FilterType.LocalBranch && filter.Pattern.Equals(oldName, StringComparison.Ordinal)) { - filter.Pattern = $"refs/heads/{newName}"; + filter.Pattern = newName; break; } } @@ -376,51 +400,8 @@ public void RemoveBranchFiltersByPrefix(string pattern) HistoryFilters.Remove(filter); } - public string BuildHistoryParams() + public string BuildHistoryParams(string gitDir) { - var includedRefs = new List(); - var excludedBranches = new List(); - var excludedRemotes = new List(); - var excludedTags = new List(); - foreach (var filter in HistoryFilters) - { - 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 (EnableTopoOrderInHistory) @@ -437,46 +418,95 @@ public string BuildHistoryParams() if (HistoryShowFlags.HasFlag(HistoryShowFlags.SimplifyByDecoration)) builder.Append("--simplify-by-decoration "); - if (includedRefs.Count > 0) + 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) { - foreach (var r in includedRefs) - { - builder.Append(r); - builder.Append(' '); - } + 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} "); } - else if (excludedBranches.Count + excludedRemotes.Count + excludedTags.Count > 0) + } + + private void BuildHistoryParamsForExcluded(StringBuilder builder, string gitDir) + { + var excludedBranches = new List(); + var excludedRemotes = new List(); + var excludedTags = new List(); + foreach (var filter in HistoryFilters) { - foreach (var b in excludedBranches) - { - builder.Append(b); - builder.Append(' '); - } + 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}\" "); + } - builder.Append("--exclude=HEAD --branches "); + foreach (var b in excludedBranches) + builder.Append(b); - foreach (var r in excludedRemotes) - { - builder.Append(r); - builder.Append(' '); - } + builder.Append("--branches "); - builder.Append("--exclude=origin/HEAD --remotes "); + 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 t in excludedTags) - { - builder.Append(t); - builder.Append(' '); - } + foreach (var r in excludedRemotes) + builder.Append(r); - builder.Append("--tags "); - } - else + 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) { - builder.Append("--branches --remotes --tags HEAD"); + RecentCommitMessages.RemoveAt(existIdx); + RecentCommitMessages.Insert(0, message); + return; } - return builder.ToString(); + 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 9a9fe7d209..86e9a1aedd 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -6,7 +6,18 @@ namespace SourceGit.Models { public class RevisionBinaryFile { + public string Repository { get; set; } = string.Empty; + public string File { get; set; } = string.Empty; + public string Revision { get; set; } = string.Empty; public long Size { get; set; } = 0; + + public RevisionBinaryFile(string repo, string file, string revision, long size) + { + Repository = repo; + File = file; + Revision = revision; + Size = size; + } } public class RevisionImageFile @@ -40,5 +51,18 @@ public class RevisionSubmodule public Commit Commit { get; set; } = null; public CommitFullMessage FullMessage { get; set; } = null; public int UncommittedChanges { get; set; } = 0; + + public RevisionSubmodule(string hash) + { + Commit = new Commit() { SHA = hash }; + FullMessage = new CommitFullMessage() { Message = string.Empty }; + } + + public RevisionSubmodule(Commit commit, string message, int uncommittedChanges) + { + Commit = commit; + FullMessage = new CommitFullMessage() { Message = message }; + UncommittedChanges = uncommittedChanges; + } } } diff --git a/src/Models/SSHKeyPair.cs b/src/Models/SSHKeyPair.cs new file mode 100644 index 0000000000..1094b03137 --- /dev/null +++ b/src/Models/SSHKeyPair.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; + +namespace SourceGit.Models +{ + public record SSHKeyType(string Name, string Description, string Cmdline) + { + public static readonly List Supported = [ + new SSHKeyType("ED25519", "Recommended", "-t ed25519"), + new SSHKeyType("RSA", "4096 Bits", "-t rsa -b 4096"), + ]; + } + + public class SSHKeyPair + { + public string PrivateKeyPath { get; set; } + public string PublicKeyPath { get; set; } + public string RawPublicKey { get; set; } + public string Fingerprint { get; set; } = "--- (invalid)"; + public string Name => Path.GetFileName(PrivateKeyPath); + + public SSHKeyPair(string privateKey, string publicKey) + { + PrivateKeyPath = privateKey; + PublicKeyPath = publicKey; + RawPublicKey = File.ReadAllText(publicKey); + + var parts = RawPublicKey.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 3) + return; + + try + { + var blob = Convert.FromBase64String(parts[1]); + Fingerprint = "SHA256:" + Convert.ToBase64String(SHA256.HashData(blob)).TrimEnd('='); + } + catch + { + // Ignore errors and keep the fingerprint as invalid + } + } + } +} diff --git a/src/Models/SafeDirectories.cs b/src/Models/SafeDirectories.cs new file mode 100644 index 0000000000..5fcf24a3af --- /dev/null +++ b/src/Models/SafeDirectories.cs @@ -0,0 +1,103 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace SourceGit.Models +{ + /// + /// Helpers for the `safe.directory` protection introduced by git 2.35.2 (CVE-2022-24765). + /// Git refuses to use a repository whose top-level directory is owned by another user (which is + /// always the case for network shares / UNC paths) unless the path is listed in the `safe.directory` config. + /// + public static partial class SafeDirectories + { + public static bool IsUntrustedRepository(string output) + { + if (string.IsNullOrWhiteSpace(output)) + return false; + + return output.Contains("detected dubious ownership", StringComparison.OrdinalIgnoreCase) || + output.Contains("unsafe repository", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Tries to get the value that should be written into the `safe.directory` config. + /// Prefers the value suggested by git itself (which knows the correct form for the current platform), + /// and falls back to building a value from the given path. + /// + public static bool TryGetSafeDirectoryValue(string path, string output, out string value) + { + value = ParseSuggestedValue(output); + if (!string.IsNullOrEmpty(value)) + return true; + + var normalized = Normalize(path); + if (string.IsNullOrEmpty(normalized)) + return false; + + // Git for Windows uses the `%(prefix)/` prefix for UNC paths. + value = OperatingSystem.IsWindows() && normalized.StartsWith("//", StringComparison.Ordinal) ? $"%(prefix)/{normalized}" : normalized; + return true; + } + + private static string Normalize(string path) + { + return path?.Replace('\\', '/').TrimEnd('/') ?? string.Empty; + } + + private static string ParseSuggestedValue(string output) + { + if (string.IsNullOrEmpty(output)) + return string.Empty; + + var match = REG_SAFE_DIRECTORY_HINT().Match(output); + if (!match.Success) + return string.Empty; + + return Dequote(match.Groups["value"].Value.Trim()); + } + + /// + /// Git quotes the suggested value with `sq_quote_buf()`, which escapes `'` and `!` as `'\''` and + /// `'\!'`. Decodes it back to the plain path (see `quote.c` of git). + /// + private static string Dequote(string value) + { + if (value.Length > 1 && value[0] == '\'') + { + var builder = new StringBuilder(value.Length); + for (var i = 1; i < value.Length; i++) + { + var c = value[i]; + if (c != '\'') + { + builder.Append(c); + continue; + } + + if (i == value.Length - 1) + return builder.ToString(); + + if (i + 3 < value.Length && value[i + 1] == '\\' && (value[i + 2] == '\'' || value[i + 2] == '!') && value[i + 3] == '\'') + { + builder.Append(value[i + 2]); + i += 3; + continue; + } + + return string.Empty; + } + + return string.Empty; + } + + if (value.Length > 1 && value[0] == '"' && value[^1] == '"') + return value[1..^1]; + + return value; + } + + [GeneratedRegex(@"--add\s+safe\.directory\s+(?[^\r\n]+)", RegexOptions.Multiline)] + private static partial Regex REG_SAFE_DIRECTORY_HINT(); + } +} diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index fa818bfe72..e8e5854754 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 diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index 700c9311b4..4ef9927fc5 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/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 38c0aaa724..bdddeb1b54 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -27,14 +27,21 @@ public static class GrammarUtility new ExtraGrammar("source.hxml", [".hxml"], "hxml.json"), new ExtraGrammar("text.html.jsp", [".jsp", ".jspf", ".tag"], "jsp.json"), new ExtraGrammar("source.vue", [".vue"], "vue.json"), + new ExtraGrammar("source.erlang", [".erl", ".escript", ".hrl"], "erlang.json"), + new ExtraGrammar("source.ocaml", [".ml", ".mli"], "ocaml.json"), + new ExtraGrammar("source.ocamllex", [".mll"], "ocamllex.json"), + new ExtraGrammar("source.ocamlyacc", [".mly"], "ocamlyacc.json"), + new ExtraGrammar("source.swift", [".swift"], "swift.json"), ]; + private static readonly Dictionary s_cachedRawGrammars = new(); + public static string GetScope(string file, RegistryOptions reg) { var extension = Path.GetExtension(file); if (extension == ".h") extension = ".cpp"; - else if (extension is ".resx" or ".plist" or ".manifest") + else if (extension is ".resx" or ".plist" or ".manifest" or ".sln" or ".slnx") extension = ".xml"; else if (extension == ".command") extension = ".sh"; @@ -53,6 +60,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 +75,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 +86,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 +105,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 +149,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 531cbccdd2..c80576dc83 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 @@ -9,7 +8,6 @@ public class ThemeOverrides public Dictionary BasicColors { get; set; } = new Dictionary(); public double GraphPenThickness { get; set; } = 2; public double OpacityForNotMergedCommits { get; set; } = 0.5; - public bool UseMicaOnWindows11 { get; set; } = true; public List GraphColors { get; set; } = new List(); } } diff --git a/src/Models/User.cs b/src/Models/User.cs index 15a45762bc..a1b984a040 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 ce18fe4be2..66e246bc07 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -43,6 +43,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) combined.Renamed += OnRepositoryChanged; combined.Changed += OnRepositoryChanged; combined.Deleted += OnRepositoryChanged; + combined.Error += OnWatcherError; combined.EnableRaisingEvents = false; _watchers.Add(combined); @@ -58,6 +59,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) wc.Renamed += OnWorkingCopyChanged; wc.Changed += OnWorkingCopyChanged; wc.Deleted += OnWorkingCopyChanged; + wc.Error += OnWatcherError; wc.EnableRaisingEvents = false; var git = new FileSystemWatcher(); @@ -69,6 +71,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) git.Renamed += OnGitDirChanged; git.Changed += OnGitDirChanged; git.Deleted += OnGitDirChanged; + git.Error += OnWatcherError; git.EnableRaisingEvents = false; _watchers.Add(wc); @@ -240,6 +243,21 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e) HandleWorkingCopyFileChanged(name, e.FullPath); } + /// + /// The internal buffer overflowed (a large fetch or checkout writes thousands of files), so + /// some events were dropped and we cannot know which. Reload everything once things settle. + /// + private void OnWatcherError(object o, ErrorEventArgs e) + { + var desired = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + Interlocked.Exchange(ref _updateStashes, desired); + Interlocked.Exchange(ref _updateWC, desired); + if (_repo.MayHaveSubmodules()) + Interlocked.Exchange(ref _updateSubmodules, desired); + } + private void OnGitDirChanged(object o, FileSystemEventArgs e) { if (string.IsNullOrEmpty(e.Name)) @@ -290,7 +308,7 @@ private void HandleGitDirFileChanged(string name) { Interlocked.Exchange(ref _updateTags, DateTime.Now.AddSeconds(.5).ToFileTime()); } - else if (name.StartsWith("refs/stash", StringComparison.Ordinal)) + else if (name.StartsWith("refs/stash", StringComparison.Ordinal) || name.Equals("logs/refs/stash", StringComparison.Ordinal)) { Interlocked.Exchange(ref _updateStashes, DateTime.Now.AddSeconds(.5).ToFileTime()); } @@ -302,6 +320,12 @@ private void HandleGitDirFileChanged(string name) { Interlocked.Exchange(ref _updateBranch, DateTime.Now.AddSeconds(.5).ToFileTime()); } + else if (name.Equals("packed-refs", StringComparison.Ordinal)) + { + var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + } else if (name.StartsWith("reftable/", StringComparison.Ordinal)) { var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index e411cbcef9..3d09f82663 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using Avalonia; @@ -13,6 +14,9 @@ namespace SourceGit.Native [SupportedOSPlatform("linux")] internal class Linux : OS.IBackend { + [DllImport("libc", SetLastError = true)] + private static extern int kill(int pid, int sig); + public void SetupApp(AppBuilder builder) { builder.With(new X11PlatformOptions() { EnableIme = true }); @@ -20,6 +24,8 @@ public void SetupApp(AppBuilder builder) public void SetupWindow(Window window) { + window.BorderThickness = new Thickness(0); + if (OS.UseSystemWindowFrame) { window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; @@ -33,53 +39,55 @@ public void SetupWindow(Window window) } } - public void HideSelf() - { - // Do Nothing. Never used. - } - - public void HideOtherApplications() + public OS.Directories GetOrCreateDirectories() { - // Do Nothing. Never used. - } - - public void ShowAllApplications() - { - // Do Nothing. Never used. - } + var dirs = new OS.Directories(); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - 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; + { + dirs.ConfigDir = portableDir; + dirs.CacheDir = portableDir; + return dirs; + } } - // Runtime data dir: ~/.sourcegit - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var dataDir = Path.Combine(home, ".sourcegit"); - if (Directory.Exists(dataDir)) - return dataDir; + // XDG Base Directory Specification: https://specifications.freedesktop.org/basedir/latest/ + dirs.ConfigDir = GetXdgDirectory("XDG_CONFIG_HOME", Path.Combine(home, ".config"), "SourceGit"); + dirs.CacheDir = GetXdgDirectory("XDG_CACHE_HOME", Path.Combine(home, ".cache"), "SourceGit"); + + // If the app basic dirs already exist, we can skip the migration step + if (Directory.Exists(dirs.ConfigDir) && Directory.Exists(dirs.CacheDir)) + return dirs; - // Migrate old data: ~/.config/SourceGit - var oldDataDir = Path.Combine(home, ".config", "SourceGit"); - if (Directory.Exists(oldDataDir)) + // Create the config and cache directories if they don't exist + if (!Directory.Exists(dirs.ConfigDir)) + Directory.CreateDirectory(dirs.ConfigDir); + if (!Directory.Exists(dirs.CacheDir)) + Directory.CreateDirectory(dirs.CacheDir); + + // Migrate legacy data dir: ~/.sourcegit to XDG standard directories + var legacyDir = Path.Combine(home, ".sourcegit"); + if (Directory.Exists(legacyDir)) { try { - Directory.Move(oldDataDir, dataDir); + File.Copy(Path.Combine(legacyDir, "preference.json"), Path.Combine(dirs.ConfigDir, "preference.json"), true); + Directory.Move(Path.Combine(legacyDir, "avatars"), Path.Combine(dirs.CacheDir, "avatars")); + Directory.Delete(legacyDir, true); } catch { - // Ignore errors + // Ignore any errors during migration } } - return dataDir; + return dirs; } public string FindGitExecutable() @@ -169,6 +177,39 @@ public void OpenWithDefaultEditor(string file) } } + public bool SupportSetSid() + { + return true; + } + + public string GetSetSidExecutable() + { + return "setsid"; + } + + public void TerminateProcess(Process proc) + { + if (kill(-proc.Id, 15) != 0) + { + // If the process already exited, we can just ignore the error. + if (Marshal.GetLastPInvokeError() == 3 /* ESRCH */) + return; + + // Actually, this will not be called since the process is + // spawned by us (EPERM will not happen), and SIGTERM (15) + // is a valid signal (EINVAL will not happen). + // See https://www.man7.org/linux/man-pages/man2/kill.2.html + try + { + proc.Kill(true); + } + catch + { + // Ignore any errors when trying to kill the process + } + } + } + private string FindExecutable(string filename) { var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; @@ -183,5 +224,14 @@ 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 GetXdgDirectory(string envVar, string fallback, string subDirName) + { + var dir = Environment.GetEnvironmentVariable(envVar); + if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) + return Path.Combine(dir, subDirName); + + return Path.Combine(fallback, subDirName); + } } } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 6431d771f3..06d09e8fa3 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -14,17 +15,13 @@ namespace SourceGit.Native [SupportedOSPlatform("macOS")] internal class MacOS : OS.IBackend { - [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_getClass")] - public static extern IntPtr objc_getClass(string name); - - [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")] - public static extern IntPtr sel_registerName(string name); - - [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] - public static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector); + [DllImport("/usr/lib/libSystem.B.dylib", SetLastError = true)] + private static extern int kill(int pid, int sig); - [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] - public static extern IntPtr objc_msgSendWithArg(IntPtr receiver, IntPtr selector, IntPtr arg); + public MacOS() + { + _setsidExecutable = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "setsid"); + } public void SetupApp(AppBuilder builder) { @@ -40,7 +37,7 @@ public void SetupApp(AppBuilder builder) else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal)) path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path; - var customPathFile = Path.Combine(OS.DataDir, "PATH"); + var customPathFile = Path.Combine(OS.BasicDirectories.ConfigDir, "PATH"); if (File.Exists(customPathFile)) { var env = File.ReadAllText(customPathFile).Trim(); @@ -55,46 +52,22 @@ public void SetupWindow(Window window) { window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome; window.ExtendClientAreaToDecorationsHint = true; + window.BorderThickness = new Thickness(0); } - public void HideSelf() - { - IntPtr nsApplicationClass = objc_getClass("NSApplication"); - IntPtr nsSharedApplicationSelector = sel_registerName("sharedApplication"); - IntPtr nsApp = objc_msgSend(nsApplicationClass, nsSharedApplicationSelector); - IntPtr nsMethodSelector = sel_registerName("hide:"); - IntPtr nsDelegateSelector = sel_registerName("delegate"); - IntPtr nsDelegate = objc_msgSend(nsApp, nsDelegateSelector); - objc_msgSendWithArg(nsApp, nsMethodSelector, nsDelegate); - } - - public void HideOtherApplications() - { - IntPtr nsApplicationClass = objc_getClass("NSApplication"); - IntPtr nsSharedApplicationSelector = sel_registerName("sharedApplication"); - IntPtr nsApp = objc_msgSend(nsApplicationClass, nsSharedApplicationSelector); - IntPtr nsMethodSelector = sel_registerName("hideOtherApplications:"); - IntPtr nsDelegateSelector = sel_registerName("delegate"); - IntPtr nsDelegate = objc_msgSend(nsApp, nsDelegateSelector); - objc_msgSendWithArg(nsApp, nsMethodSelector, nsDelegate); - } - - public void ShowAllApplications() - { - IntPtr nsApplicationClass = objc_getClass("NSApplication"); - IntPtr nsSharedApplicationSelector = sel_registerName("sharedApplication"); - IntPtr nsApp = objc_msgSend(nsApplicationClass, nsSharedApplicationSelector); - IntPtr nsMethodSelector = sel_registerName("unhideAllApplications:"); - IntPtr nsDelegateSelector = sel_registerName("delegate"); - IntPtr nsDelegate = objc_msgSend(nsApp, nsDelegateSelector); - objc_msgSendWithArg(nsApp, nsMethodSelector, nsDelegate); - } - - public string GetDataDir() + public OS.Directories GetOrCreateDirectories() { - return Path.Combine( + var dirs = new OS.Directories(); + var appDataDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit"); + + if (!Directory.Exists(appDataDir)) + Directory.CreateDirectory(appDataDir); + + dirs.ConfigDir = appDataDir; + dirs.CacheDir = appDataDir; + return dirs; } public string FindGitExecutable() @@ -156,5 +129,218 @@ public void OpenWithDefaultEditor(string file) { Process.Start("open", file.Quoted()); } + + public bool SupportSetSid() + { + return File.Exists(_setsidExecutable); + } + + public string GetSetSidExecutable() + { + return _setsidExecutable; + } + + public void TerminateProcess(Process proc) + { + if (!SupportSetSid()) + { + proc.Kill(true); + return; + } + + if (kill(-proc.Id, 15) != 0) + { + // If the process already exited, we can just ignore the error. + if (Marshal.GetLastPInvokeError() == 3 /* ESRCH */) + return; + + // Actually, this will not be called since the process is + // spawned by us (EPERM will not happen), and SIGTERM (15) + // is a valid signal (EINVAL will not happen). + // See https://www.man7.org/linux/man-pages/man2/kill.2.html + try + { + proc.Kill(true); + } + catch + { + // Ignore any errors when trying to kill the process + } + } + } + + private string _setsidExecutable = null; + } + + [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; + } + + var yOffset = OperatingSystem.IsMacOSVersionAtLeast(26) ? 6 : 2; + objc_msgSend_Void_Point(nsCloseBtn, s_selSetFrameOrigin, new(14, yOffset)); + objc_msgSend_Void_Point(nsMinBtn, s_selSetFrameOrigin, new(14 + 20, yOffset)); + objc_msgSend_Void_Point(nsZoomBtn, s_selSetFrameOrigin, new(14 + 40, yOffset)); + } + + 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 c445e60c43..741fcf8440 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; @@ -13,16 +14,18 @@ namespace SourceGit.Native { public static partial class OS { + public class Directories + { + public string ConfigDir { get; set; } = string.Empty; + public string CacheDir { get; set; } = string.Empty; + } + public interface IBackend { void SetupApp(AppBuilder builder); void SetupWindow(Window window); - void HideSelf(); - void HideOtherApplications(); - void ShowAllApplications(); - - string GetDataDir(); + Directories GetOrCreateDirectories(); string FindGitExecutable(); string FindTerminal(Models.ShellOrTerminal shell); List FindExternalTools(); @@ -31,13 +34,17 @@ public interface IBackend void OpenInFileManager(string path); void OpenBrowser(string url); void OpenWithDefaultEditor(string file); + + bool SupportSetSid(); + string GetSetSidExecutable(); + void TerminateProcess(Process proc); } - public static string DataDir + public static Directories BasicDirectories { get; private set; - } = string.Empty; + } = new(); public static string GitExecutable { @@ -64,6 +71,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; @@ -130,11 +143,9 @@ static OS() throw new PlatformNotSupportedException(); } - public static void SetupDataDir() + public static void SetupBasicDirectories() { - DataDir = _backend.GetDataDir(); - if (!Directory.Exists(DataDir)) - Directory.CreateDirectory(DataDir); + BasicDirectories = _backend.GetOrCreateDirectories(); } public static void SetupApp(AppBuilder builder) @@ -152,27 +163,12 @@ public static void SetupForWindow(Window window) _backend.SetupWindow(window); } - public static void HideSelf() - { - _backend.HideSelf(); - } - - public static void HideOtherApplications() - { - _backend.HideOtherApplications(); - } - - public static void ShowAllApplications() - { - _backend.ShowAllApplications(); - } - public static void LogException(Exception ex) { if (ex == null) return; - var crashDir = Path.Combine(DataDir, "crashes"); + var crashDir = Path.Combine(BasicDirectories.CacheDir, "crashes"); if (!Directory.Exists(crashDir)) Directory.CreateDirectory(crashDir); @@ -254,9 +250,26 @@ public static void OpenInFileManager(string 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)) @@ -292,12 +305,28 @@ public static string GetRelativePathToHome(string path) return path; } + public static bool SupportSetSid() + { + return _backend.SupportSetSid(); + } + + public static string GetSetSidExecutable() + { + return _backend.GetSetSidExecutable(); + } + + public static void TerminateProcess(Process proc) + { + _backend.TerminateProcess(proc); + } + private static void UpdateGitVersion() { if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable)) { GitVersionString = string.Empty; GitVersion = new Version(0, 0, 0); + GitFlowVersion = Models.GitFlowVersion.None; return; } @@ -329,6 +358,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 35b8aba662..fde6cb25a7 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -17,18 +17,6 @@ namespace SourceGit.Native [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { - [StructLayout(LayoutKind.Sequential)] - internal struct MARGINS - { - public int cxLeftWidth; - public int cxRightWidth; - public int cyTopHeight; - public int cyBottomHeight; - } - - [DllImport("dwmapi.dll")] - private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); - [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs); @@ -41,14 +29,21 @@ 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("kernel32.dll")] + private static extern bool SetConsoleCtrlHandler(IntPtr handler, bool add); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AttachConsole(int dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, int dwProcessGroupId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FreeConsole(); + 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) @@ -56,33 +51,33 @@ public void SetupWindow(Window window) window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; window.ExtendClientAreaToDecorationsHint = true; window.BorderThickness = new Thickness(1); + window.Padding = new Thickness(0); } - public void HideSelf() - { - // Do Nothing. Never used. - } - - public void HideOtherApplications() - { - // Do Nothing. Never used. - } - - public void ShowAllApplications() + public OS.Directories GetOrCreateDirectories() { - // Do Nothing. Never used. - } - - public string GetDataDir() - { - var execFile = Process.GetCurrentProcess().MainModule!.FileName; + var dirs = new OS.Directories(); + var execFile = Environment.ProcessPath; var portableDir = Path.Combine(Path.GetDirectoryName(execFile)!, "data"); if (Directory.Exists(portableDir)) - return portableDir; + { + dirs.ConfigDir = portableDir; + dirs.CacheDir = portableDir; + } + else + { + var appDataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "SourceGit"); - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "SourceGit"); + if (!Directory.Exists(appDataDir)) + Directory.CreateDirectory(appDataDir); + + dirs.ConfigDir = appDataDir; + dirs.CacheDir = appDataDir; + } + + return dirs; } public string FindGitExecutable() @@ -158,17 +153,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.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); } @@ -226,22 +222,42 @@ public void OpenWithDefaultEditor(string file) Process.Start(start); } - #region HELPER_METHODS - private void FixWindowFrameOnWin10(Window w) + public bool SupportSetSid() { - // Schedule the DWM frame extension to run in the next render frame - // to ensure proper timing with the window initialization sequence - Dispatcher.UIThread.Post(() => + return false; + } + + public string GetSetSidExecutable() + { + return null; + } + + public void TerminateProcess(Process proc) + { + var pid = proc.Id; + if (AttachConsole(pid)) { - var platformHandle = w.TryGetPlatformHandle(); - if (platformHandle == null) - return; + SetConsoleCtrlHandler(IntPtr.Zero, true); + try + { + if (!GenerateConsoleCtrlEvent(0, 0)) + proc.Kill(true); - var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 }; - DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); - }, DispatcherPriority.Render); + proc.WaitForExit(2000); + } + finally + { + FreeConsole(); + SetConsoleCtrlHandler(IntPtr.Zero, false); + } + } + else + { + proc.Kill(true); + } } + #region HELPER_METHODS private List GenerateVSProjectLaunchOptions(string path) { var root = new DirectoryInfo(path); @@ -386,7 +402,7 @@ private void FindVisualStudio(Models.ExternalToolsFinder finder) { var exec = instance.ProductPath; var icon = instance.IsPrerelease ? "vs-preview" : "vs"; - finder.TryAdd(instance.DisplayName, icon, () => exec, GenerateVSProjectLaunchOptions); + finder.TryAdd(instance.DisplayName, icon, () => exec, GenerateVSProjectLaunchOptions, false); } } } @@ -415,4 +431,47 @@ private string FindZed() } #endregion } + + [SupportedOSPlatform("windows")] + public static class Win64Utilities + { + [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); + + public static void FixWindowFrame(Window w) + { + if (w.WindowState == WindowState.Maximized) + { + w.BorderThickness = new Thickness(0); + w.Padding = new Thickness(8, 6, 8, 8); + } + else + { + w.BorderThickness = new Thickness(1); + w.Padding = new Thickness(0); + } + + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) + return; + + 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); + } + } } diff --git a/src/Resources/Fonts.axaml b/src/Resources/Fonts.axaml new file mode 100644 index 0000000000..832026bead --- /dev/null +++ b/src/Resources/Fonts.axaml @@ -0,0 +1,5 @@ + + fonts:Inter#Inter + fonts:SourceGit#JetBrains Mono NL + diff --git a/src/Resources/Fonts/JetBrainsMono-Bold.ttf b/src/Resources/Fonts/JetBrainsMono-Bold.ttf deleted file mode 100644 index 8c93043de6..0000000000 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 ccc9d6a5bc..0000000000 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 dff66cc507..0000000000 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 0000000000..f78f84fb43 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 0000000000..4e9c3802dd 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 0000000000..70d2ec9e28 Binary files /dev/null and b/src/Resources/Fonts/JetBrainsMonoNL-Regular.ttf differ diff --git a/src/Resources/Grammars/erlang.json b/src/Resources/Grammars/erlang.json new file mode 100644 index 0000000000..216086ce5f --- /dev/null +++ b/src/Resources/Grammars/erlang.json @@ -0,0 +1,1753 @@ +{ + "comment": "The recognition of function definitions and compiler directives (such as module, record and macro definitions) requires that each of the aforementioned constructs must be the first string inside a line (except for whitespace). Also, the function/module/record/macro names must be given unquoted. -- desp", + "fileTypes": ["erl", "escript", "hrl", "xrl", "yrl"], + "keyEquivalent": "^~E", + "name": "erlang", + "patterns": [ + { + "include": "#module-directive" + }, + { + "include": "#import-export-directive" + }, + { + "include": "#behaviour-directive" + }, + { + "include": "#record-directive" + }, + { + "include": "#define-directive" + }, + { + "include": "#macro-directive" + }, + { + "include": "#directive" + }, + { + "include": "#function" + }, + { + "include": "#everything-else" + } + ], + "repository": { + "atom": { + "patterns": [ + { + "begin": "(')", + "beginCaptures": { + "1": { + "name": "punctuation.definition.symbol.begin.erlang" + } + }, + "end": "(')", + "endCaptures": { + "1": { + "name": "punctuation.definition.symbol.end.erlang" + } + }, + "name": "constant.other.symbol.quoted.single.erlang", + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.escape.erlang" + }, + "3": { + "name": "punctuation.definition.escape.erlang" + } + }, + "match": "(\\\\)([bdefnrstv\\\\'\"]|(\\^)[@-_a-z]|[0-7]{1,3}|x[\\da-fA-F]{2})", + "name": "constant.other.symbol.escape.erlang" + }, + { + "match": "\\\\\\^?.?", + "name": "invalid.illegal.atom.erlang" + } + ] + }, + { + "match": "[a-z][a-zA-Z\\d@_]*+", + "name": "constant.other.symbol.unquoted.erlang" + } + ] + }, + "behaviour-directive": { + "captures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.behaviour.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.type.class.behaviour.definition.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "6": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "match": "^\\s*+(-)\\s*+(behaviour)\\s*+(\\()\\s*+([a-z][a-zA-Z\\d@_]*+)\\s*+(\\))\\s*+(\\.)", + "name": "meta.directive.behaviour.erlang" + }, + "binary": { + "begin": "(<<)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.binary.begin.erlang" + } + }, + "end": "(>>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.binary.end.erlang" + } + }, + "name": "meta.structure.binary.erlang", + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.separator.binary.erlang" + }, + "2": { + "name": "punctuation.separator.value-size.erlang" + } + }, + "match": "(,)|(:)" + }, + { + "include": "#internal-type-specifiers" + }, + { + "include": "#everything-else" + } + ] + }, + "character": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.character.erlang" + }, + "2": { + "name": "constant.character.escape.erlang" + }, + "3": { + "name": "punctuation.definition.escape.erlang" + }, + "5": { + "name": "punctuation.definition.escape.erlang" + } + }, + "match": "(\\$)((\\\\)([bdefnrstv\\\\'\"]|(\\^)[@-_a-z]|[0-7]{1,3}|x[\\da-fA-F]{2}))", + "name": "constant.character.erlang" + }, + { + "match": "\\$\\\\\\^?.?", + "name": "invalid.illegal.character.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.character.erlang" + } + }, + "match": "(\\$)[ \\S]", + "name": "constant.character.erlang" + }, + { + "match": "\\$.?", + "name": "invalid.illegal.character.erlang" + } + ] + }, + "comment": { + "begin": "(^[ \\t]+)?(?=%)", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.erlang" + } + }, + "end": "(?!\\G)", + "patterns": [ + { + "begin": "%", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.erlang" + } + }, + "end": "\\n", + "name": "comment.line.percentage.erlang" + } + ] + }, + "define-directive": { + "patterns": [ + { + "begin": "^\\s*+(-)\\s*+(define)\\s*+(\\()\\s*+([a-zA-Z\\d@_]++)\\s*+", + "beginCaptures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.define.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.function.macro.definition.erlang" + } + }, + "end": "(\\))\\s*+(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "name": "meta.directive.define.erlang", + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "begin": "(?=^\\s*+-\\s*+define\\s*+\\(\\s*+[a-zA-Z\\d@_]++\\s*+\\()", + "end": "(\\))\\s*+(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "name": "meta.directive.define.erlang", + "patterns": [ + { + "begin": "^\\s*+(-)\\s*+(define)\\s*+(\\()\\s*+([a-zA-Z\\d@_]++)\\s*+(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.define.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.function.macro.definition.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "end": "(\\))\\s*(,)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.separator.parameters.erlang" + } + }, + "patterns": [ + { + "match": ",", + "name": "punctuation.separator.parameters.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + { + "match": "\\|\\||\\||:|;|,|\\.|->", + "name": "punctuation.separator.define.erlang" + }, + { + "include": "#everything-else" + } + ] + } + ] + }, + "directive": { + "patterns": [ + { + "begin": "^\\s*+(-)\\s*+([a-z][a-zA-Z\\d@_]*+)\\s*+(\\(?)", + "beginCaptures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "end": "(\\)?)\\s*+(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "name": "meta.directive.erlang", + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "captures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.erlang" + }, + "3": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "match": "^\\s*+(-)\\s*+([a-z][a-zA-Z\\d@_]*+)\\s*+(\\.)", + "name": "meta.directive.erlang" + } + ] + }, + "everything-else": { + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#record-usage" + }, + { + "include": "#macro-usage" + }, + { + "include": "#expression" + }, + { + "include": "#keyword" + }, + { + "include": "#textual-operator" + }, + { + "include": "#language-constant" + }, + { + "include": "#function-call" + }, + { + "include": "#tuple" + }, + { + "include": "#list" + }, + { + "include": "#binary" + }, + { + "include": "#parenthesized-expression" + }, + { + "include": "#character" + }, + { + "include": "#number" + }, + { + "include": "#atom" + }, + { + "include": "#string" + }, + { + "include": "#symbolic-operator" + }, + { + "include": "#variable" + } + ] + }, + "expression": { + "patterns": [ + { + "begin": "\\b(if)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.if.erlang" + } + }, + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.if.erlang", + "patterns": [ + { + "include": "#internal-expression-punctuation" + }, + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(case)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.case.erlang" + } + }, + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.case.erlang", + "patterns": [ + { + "include": "#internal-expression-punctuation" + }, + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(receive)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.receive.erlang" + } + }, + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.receive.erlang", + "patterns": [ + { + "include": "#internal-expression-punctuation" + }, + { + "include": "#everything-else" + } + ] + }, + { + "captures": { + "1": { + "name": "keyword.control.fun.erlang" + }, + "4": { + "name": "entity.name.type.class.module.erlang" + }, + "5": { + "name": "variable.other.erlang" + }, + "6": { + "name": "punctuation.separator.module-function.erlang" + }, + "8": { + "name": "entity.name.function.erlang" + }, + "9": { + "name": "variable.other.erlang" + }, + "10": { + "name": "punctuation.separator.function-arity.erlang" + } + }, + "comment": "Implicit function expression with optional module qualifier when both module and function can be atom or variable", + "match": "\\b(fun)\\s+((([a-z][a-zA-Z\\d@_]*+)|(_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+))\\s*+(:)\\s*+)?(([a-z][a-zA-Z\\d@_]*+|'[^']*+')|(_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+))\\s*(/)", + "name": "meta.expression.fun.implicit.erlang" + }, + { + "begin": "\\b(fun)\\s+(([a-z][a-zA-Z\\d@_]*+)|(_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+))\\s*+(:)", + "beginCaptures": { + "1": { + "name": "keyword.control.fun.erlang" + }, + "3": { + "name": "entity.name.type.class.module.erlang" + }, + "4": { + "name": "variable.other.erlang" + }, + "5": { + "name": "punctuation.separator.module-function.erlang" + } + }, + "comment": "Implicit function expression with module qualifier when module can be atom or variable and function can by anything", + "end": "(/)", + "endCaptures": { + "1": { + "name": "punctuation.separator.function-arity.erlang" + } + }, + "name": "meta.expression.fun.implicit.erlang", + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(fun)\\s+(?!\\()", + "beginCaptures": { + "1": { + "name": "keyword.control.fun.erlang" + } + }, + "comment": "Implicit function expression when both module and function can by anything", + "end": "(/)", + "endCaptures": { + "1": { + "name": "punctuation.separator.function-arity.erlang" + } + }, + "name": "meta.expression.fun.implicit.erlang", + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(fun)\\s*+(\\()(?=(\\s*+\\())", + "beginCaptures": { + "1": { + "name": "entity.name.function.erlang" + }, + "2": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "comment": "Function type in type specification", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + } + }, + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(fun)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.fun.erlang" + } + }, + "comment": "Explicit function expression", + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.fun.erlang", + "patterns": [ + { + "begin": "(?=\\()", + "end": "(;)|(?=\\bend\\b)", + "endCaptures": { + "1": { + "name": "punctuation.separator.clauses.erlang" + } + }, + "patterns": [ + { + "include": "#internal-function-parts" + } + ] + }, + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(try)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.try.erlang" + } + }, + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.try.erlang", + "patterns": [ + { + "include": "#internal-expression-punctuation" + }, + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(begin)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.begin.erlang" + } + }, + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.begin.erlang", + "patterns": [ + { + "include": "#internal-expression-punctuation" + }, + { + "include": "#everything-else" + } + ] + }, + { + "begin": "\\b(maybe)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.maybe.erlang" + } + }, + "end": "\\b(end)\\b", + "endCaptures": { + "1": { + "name": "keyword.control.end.erlang" + } + }, + "name": "meta.expression.maybe.erlang", + "patterns": [ + { + "include": "#internal-expression-punctuation" + }, + { + "include": "#everything-else" + } + ] + } + ] + }, + "function": { + "begin": "^\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')\\s*+(?=\\()", + "beginCaptures": { + "1": { + "name": "entity.name.function.definition.erlang" + } + }, + "end": "(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.terminator.function.erlang" + } + }, + "name": "meta.function.erlang", + "patterns": [ + { + "captures": { + "1": { + "name": "entity.name.function.erlang" + } + }, + "match": "^\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')\\s*+(?=\\()" + }, + { + "begin": "(?=\\()", + "end": "(;)|(?=\\.)", + "endCaptures": { + "1": { + "name": "punctuation.separator.clauses.erlang" + } + }, + "patterns": [ + { + "include": "#parenthesized-expression" + }, + { + "include": "#internal-function-parts" + } + ] + }, + { + "include": "#everything-else" + } + ] + }, + "function-call": { + "begin": "(?=([a-z][a-zA-Z\\d@_]*+|'[^']*+'|_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+)\\s*+(\\(|:\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+'|_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+)\\s*+\\())", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + } + }, + "name": "meta.function-call.erlang", + "patterns": [ + { + "begin": "((erlang)\\s*+(:)\\s*+)?(is_atom|is_binary|is_constant|is_float|is_function|is_integer|is_list|is_number|is_pid|is_port|is_reference|is_tuple|is_record|abs|element|hd|length|node|round|self|size|tl|trunc)\\s*+(\\()", + "beginCaptures": { + "2": { + "name": "entity.name.type.class.module.erlang" + }, + "3": { + "name": "punctuation.separator.module-function.erlang" + }, + "4": { + "name": "entity.name.function.guard.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "match": ",", + "name": "punctuation.separator.parameters.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + { + "begin": "((([a-z][a-zA-Z\\d@_]*+|'[^']*+')|(_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+))\\s*+(:)\\s*+)?(([a-z][a-zA-Z\\d@_]*+|'[^']*+')|(_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+))\\s*+(\\()", + "beginCaptures": { + "3": { + "name": "entity.name.type.class.module.erlang" + }, + "4": { + "name": "variable.other.erlang" + }, + "5": { + "name": "punctuation.separator.module-function.erlang" + }, + "7": { + "name": "entity.name.function.erlang" + }, + "8": { + "name": "variable.other.erlang" + }, + "9": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "match": ",", + "name": "punctuation.separator.parameters.erlang" + }, + { + "include": "#everything-else" + } + ] + } + ] + }, + "import-export-directive": { + "patterns": [ + { + "begin": "^\\s*+(-)\\s*+(import)\\s*+(\\()\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')\\s*+(,)", + "beginCaptures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.import.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.type.class.module.erlang" + }, + "5": { + "name": "punctuation.separator.parameters.erlang" + } + }, + "end": "(\\))\\s*+(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "name": "meta.directive.import.erlang", + "patterns": [ + { + "include": "#internal-function-list" + } + ] + }, + { + "begin": "^\\s*+(-)\\s*+(export)\\s*+(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.export.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "end": "(\\))\\s*+(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "name": "meta.directive.export.erlang", + "patterns": [ + { + "include": "#internal-function-list" + } + ] + } + ] + }, + "internal-expression-punctuation": { + "captures": { + "1": { + "name": "punctuation.separator.clause-head-body.erlang" + }, + "2": { + "name": "punctuation.separator.clauses.erlang" + }, + "3": { + "name": "punctuation.separator.expressions.erlang" + } + }, + "match": "(->)|(;)|(,)" + }, + "internal-function-list": { + "begin": "(\\[)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.list.begin.erlang" + } + }, + "end": "(\\])", + "endCaptures": { + "1": { + "name": "punctuation.definition.list.end.erlang" + } + }, + "name": "meta.structure.list.function.erlang", + "patterns": [ + { + "begin": "([a-z][a-zA-Z\\d@_]*+|'[^']*+')\\s*+(/)", + "beginCaptures": { + "1": { + "name": "entity.name.function.erlang" + }, + "2": { + "name": "punctuation.separator.function-arity.erlang" + } + }, + "end": "(,)|(?=\\])", + "endCaptures": { + "1": { + "name": "punctuation.separator.list.erlang" + } + }, + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "include": "#everything-else" + } + ] + }, + "internal-function-parts": { + "patterns": [ + { + "begin": "(?=\\()", + "end": "(->)", + "endCaptures": { + "1": { + "name": "punctuation.separator.clause-head-body.erlang" + } + }, + "patterns": [ + { + "begin": "(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.definition.parameters.begin.erlang" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + } + }, + "patterns": [ + { + "match": ",", + "name": "punctuation.separator.parameters.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + { + "match": ",|;", + "name": "punctuation.separator.guards.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + { + "match": ",", + "name": "punctuation.separator.expressions.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + "internal-record-body": { + "begin": "(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.class.record.begin.erlang" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.class.record.end.erlang" + } + }, + "name": "meta.structure.record.erlang", + "patterns": [ + { + "begin": "(([a-z][a-zA-Z\\d@_]*+|'[^']*+')|(_))", + "beginCaptures": { + "2": { + "name": "variable.other.field.erlang" + }, + "3": { + "name": "variable.language.omitted.field.erlang" + } + }, + "end": "(,)|(?=\\})", + "endCaptures": { + "1": { + "name": "punctuation.separator.class.record.erlang" + } + }, + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + { + "include": "#everything-else" + } + ] + }, + "internal-type-specifiers": { + "begin": "(/)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.value-type.erlang" + } + }, + "end": "(?=,|:|>>)", + "patterns": [ + { + "captures": { + "1": { + "name": "storage.type.erlang" + }, + "2": { + "name": "storage.modifier.signedness.erlang" + }, + "3": { + "name": "storage.modifier.endianness.erlang" + }, + "4": { + "name": "storage.modifier.unit.erlang" + }, + "5": { + "name": "punctuation.separator.unit-specifiers.erlang" + }, + "6": { + "name": "constant.numeric.integer.decimal.erlang" + }, + "7": { + "name": "punctuation.separator.type-specifiers.erlang" + } + }, + "match": "(integer|float|binary|bytes|bitstring|bits|utf8|utf16|utf32)|(signed|unsigned)|(big|little|native)|(unit)(:)(\\d++)|(-)" + } + ] + }, + "keyword": { + "match": "\\b(after|begin|case|catch|cond|end|fun|if|let|of|try|receive|when|maybe|else)\\b", + "name": "keyword.control.erlang" + }, + "language-constant": { + "match": "\\b(false|true|undefined)\\b", + "name": "constant.language" + }, + "list": { + "begin": "(\\[)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.list.begin.erlang" + } + }, + "end": "(\\])", + "endCaptures": { + "1": { + "name": "punctuation.definition.list.end.erlang" + } + }, + "name": "meta.structure.list.erlang", + "patterns": [ + { + "match": "\\||\\|\\||,", + "name": "punctuation.separator.list.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + "macro-directive": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.ifdef.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.function.macro.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "6": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "match": "^\\s*+(-)\\s*+(ifdef)\\s*+(\\()\\s*+([a-zA-z\\d@_]++)\\s*+(\\))\\s*+(\\.)", + "name": "meta.directive.ifdef.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.ifndef.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.function.macro.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "6": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "match": "^\\s*+(-)\\s*+(ifndef)\\s*+(\\()\\s*+([a-zA-z\\d@_]++)\\s*+(\\))\\s*+(\\.)", + "name": "meta.directive.ifndef.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.undef.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.function.macro.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "6": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "match": "^\\s*+(-)\\s*+(undef)\\s*+(\\()\\s*+([a-zA-z\\d@_]++)\\s*+(\\))\\s*+(\\.)", + "name": "meta.directive.undef.erlang" + } + ] + }, + "macro-usage": { + "captures": { + "1": { + "name": "keyword.operator.macro.erlang" + }, + "2": { + "name": "entity.name.function.macro.erlang" + } + }, + "match": "(\\?\\??)\\s*+([a-zA-Z\\d@_]++)", + "name": "meta.macro-usage.erlang" + }, + "module-directive": { + "captures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.module.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.type.class.module.definition.erlang" + }, + "5": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "6": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "match": "^\\s*+(-)\\s*+(module)\\s*+(\\()\\s*+([a-z][a-zA-Z\\d@_]*+)\\s*+(\\))\\s*+(\\.)", + "name": "meta.directive.module.erlang" + }, + "number": { + "begin": "(?=\\d)", + "end": "(?!\\d)", + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.separator.integer-float.erlang" + }, + "2": { + "name": "punctuation.separator.float-exponent.erlang" + } + }, + "match": "\\d++(\\.)\\d++([eE][\\+\\-]?\\d++)?", + "name": "constant.numeric.float.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "2(#)([0-1]++_)*[0-1]++", + "name": "constant.numeric.integer.binary.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "3(#)([0-2]++_)*[0-2]++", + "name": "constant.numeric.integer.base-3.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "4(#)([0-3]++_)*[0-3]++", + "name": "constant.numeric.integer.base-4.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "5(#)([0-4]++_)*[0-4]++", + "name": "constant.numeric.integer.base-5.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "6(#)([0-5]++_)*[0-5]++", + "name": "constant.numeric.integer.base-6.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "7(#)([0-6]++_)*[0-6]++", + "name": "constant.numeric.integer.base-7.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "8(#)([0-7]++_)*[0-7]++", + "name": "constant.numeric.integer.octal.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "9(#)([0-8]++_)*[0-8]++", + "name": "constant.numeric.integer.base-9.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "10(#)(\\d++_)*\\d++", + "name": "constant.numeric.integer.decimal.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "11(#)([\\daA]++_)*[\\daA]++", + "name": "constant.numeric.integer.base-11.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "12(#)([\\da-bA-B]++_)*[\\da-bA-B]++", + "name": "constant.numeric.integer.base-12.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "13(#)([\\da-cA-C]++_)*[\\da-cA-C]++", + "name": "constant.numeric.integer.base-13.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "14(#)([\\da-dA-D]++_)*[\\da-dA-D]++", + "name": "constant.numeric.integer.base-14.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "15(#)([\\da-eA-E]++_)*[\\da-eA-E]++", + "name": "constant.numeric.integer.base-15.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "16(#)([\\da-fA-F]++_)*[\\da-fA-F]++", + "name": "constant.numeric.integer.hexadecimal.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "17(#)([\\da-gA-G]++_)*[\\da-gA-G]++", + "name": "constant.numeric.integer.base-17.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "18(#)([\\da-hA-H]++_)*[\\da-hA-H]++", + "name": "constant.numeric.integer.base-18.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "19(#)([\\da-iA-I]++_)*[\\da-iA-I]++", + "name": "constant.numeric.integer.base-19.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "20(#)([\\da-jA-J]++_)*[\\da-jA-J]++", + "name": "constant.numeric.integer.base-20.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "21(#)([\\da-kA-K]++_)*[\\da-kA-K]++", + "name": "constant.numeric.integer.base-21.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "22(#)([\\da-lA-L]++_)*[\\da-lA-L]++", + "name": "constant.numeric.integer.base-22.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "23(#)([\\da-mA-M]++_)*[\\da-mA-M]++", + "name": "constant.numeric.integer.base-23.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "24(#)([\\da-nA-N]++_)*[\\da-nA-N]++", + "name": "constant.numeric.integer.base-24.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "25(#)([\\da-oA-O]++_)*[\\da-oA-O]++", + "name": "constant.numeric.integer.base-25.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "26(#)([\\da-pA-P]++_)*[\\da-pA-P]++", + "name": "constant.numeric.integer.base-26.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "27(#)([\\da-qA-Q]++_)*[\\da-qA-Q]++", + "name": "constant.numeric.integer.base-27.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "28(#)([\\da-rA-R]++_)*[\\da-rA-R]++", + "name": "constant.numeric.integer.base-28.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "29(#)([\\da-sA-S]++_)*[\\da-sA-S]++", + "name": "constant.numeric.integer.base-29.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "30(#)([\\da-tA-T]++_)*[\\da-tA-T]++", + "name": "constant.numeric.integer.base-30.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "31(#)([\\da-uA-U]++_)*[\\da-uA-U]++", + "name": "constant.numeric.integer.base-31.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "32(#)([\\da-vA-V]++_)*[\\da-vA-V]++", + "name": "constant.numeric.integer.base-32.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "33(#)([\\da-wA-W]++_)*[\\da-wA-W]++", + "name": "constant.numeric.integer.base-33.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "34(#)([\\da-xA-X]++_)*[\\da-xA-X]++", + "name": "constant.numeric.integer.base-34.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "35(#)([\\da-yA-Y]++_)*[\\da-yA-Y]++", + "name": "constant.numeric.integer.base-35.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.separator.base-integer.erlang" + } + }, + "match": "36(#)([\\da-zA-Z]++_)*[\\da-zA-Z]++", + "name": "constant.numeric.integer.base-36.erlang" + }, + { + "match": "\\d++#([\\da-zA-Z]++_)*[\\da-zA-Z]++", + "name": "invalid.illegal.integer.erlang" + }, + { + "match": "(\\d++_)*\\d++", + "name": "constant.numeric.integer.decimal.erlang" + } + ] + }, + "parenthesized-expression": { + "begin": "(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.section.expression.begin.erlang" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.section.expression.end.erlang" + } + }, + "name": "meta.expression.parenthesized", + "patterns": [ + { + "include": "#everything-else" + } + ] + }, + "record-directive": { + "begin": "^\\s*+(-)\\s*+(record)\\s*+(\\()\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')\\s*+(,)", + "beginCaptures": { + "1": { + "name": "punctuation.section.directive.begin.erlang" + }, + "2": { + "name": "keyword.control.directive.import.erlang" + }, + "3": { + "name": "punctuation.definition.parameters.begin.erlang" + }, + "4": { + "name": "entity.name.type.class.record.definition.erlang" + }, + "5": { + "name": "punctuation.separator.parameters.erlang" + } + }, + "end": "(\\))\\s*+(\\.)", + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.erlang" + }, + "2": { + "name": "punctuation.section.directive.end.erlang" + } + }, + "name": "meta.directive.record.erlang", + "patterns": [ + { + "include": "#internal-record-body" + }, + { + "include": "#comment" + } + ] + }, + "record-usage": { + "patterns": [ + { + "captures": { + "1": { + "name": "keyword.operator.record.erlang" + }, + "2": { + "name": "entity.name.type.class.record.erlang" + }, + "3": { + "name": "punctuation.separator.record-field.erlang" + }, + "4": { + "name": "variable.other.field.erlang" + } + }, + "match": "(#)\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')\\s*+(\\.)\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')", + "name": "meta.record-usage.erlang" + }, + { + "begin": "(#)\\s*+([a-z][a-zA-Z\\d@_]*+|'[^']*+')", + "beginCaptures": { + "1": { + "name": "keyword.operator.record.erlang" + }, + "2": { + "name": "entity.name.type.class.record.erlang" + } + }, + "end": "(?<=\\})", + "name": "meta.record-usage.erlang", + "patterns": [ + { + "include": "#internal-record-body" + } + ] + } + ] + }, + "string": { + "begin": "(\")", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.erlang" + } + }, + "end": "(\")", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.erlang" + } + }, + "name": "string.quoted.double.erlang", + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.escape.erlang" + }, + "3": { + "name": "punctuation.definition.escape.erlang" + } + }, + "match": "(\\\\)([bdefnrstv\\\\'\"]|(\\^)[@-_a-z]|[0-7]{1,3}|x[\\da-fA-F]{2})", + "name": "constant.character.escape.erlang" + }, + { + "match": "\\\\\\^?.?", + "name": "invalid.illegal.string.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.placeholder.erlang" + }, + "3": { + "name": "punctuation.separator.placeholder-parts.erlang" + }, + "4": { + "name": "punctuation.separator.placeholder-parts.erlang" + }, + "6": { + "name": "punctuation.separator.placeholder-parts.erlang" + }, + "8": { + "name": "punctuation.separator.placeholder-parts.erlang" + }, + "10": { + "name": "punctuation.separator.placeholder-parts.erlang" + }, + "12": { + "name": "punctuation.separator.placeholder-parts.erlang" + } + }, + "match": "(~)((\\-)?\\d++|(\\*))?((\\.)(\\d++|(\\*)))?((\\.)((\\*)|.))?[~cfegswpWPBX#bx\\+ni]", + "name": "constant.other.placeholder.erlang" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.placeholder.erlang" + }, + "2": { + "name": "punctuation.separator.placeholder-parts.erlang" + } + }, + "match": "(~)(\\*)?(\\d++)?[~du\\-#fsacl]", + "name": "constant.other.placeholder.erlang" + }, + { + "match": "~[^\"]?", + "name": "invalid.illegal.string.erlang" + } + ] + }, + "symbolic-operator": { + "match": "\\+\\+|\\+|--|-|\\*|/=|/|=/=|=:=|==|=<|=|<-|<|>=|>|!|::|\\?=", + "name": "keyword.operator.symbolic.erlang" + }, + "textual-operator": { + "match": "\\b(andalso|band|and|bxor|xor|bor|orelse|or|bnot|not|bsl|bsr|div|rem)\\b", + "name": "keyword.operator.textual.erlang" + }, + "tuple": { + "begin": "(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tuple.begin.erlang" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.tuple.end.erlang" + } + }, + "name": "meta.structure.tuple.erlang", + "patterns": [ + { + "match": ",", + "name": "punctuation.separator.tuple.erlang" + }, + { + "include": "#everything-else" + } + ] + }, + "variable": { + "captures": { + "1": { + "name": "variable.other.erlang" + }, + "2": { + "name": "variable.language.omitted.erlang" + } + }, + "match": "(_[a-zA-Z\\d@_]++|[A-Z][a-zA-Z\\d@_]*+)|(_)" + } + }, + "scopeName": "source.erlang", + "uuid": "58EA597D-5158-4BF7-9FB2-B05135D1E166" +} \ No newline at end of file diff --git a/src/Resources/Grammars/ocaml.json b/src/Resources/Grammars/ocaml.json new file mode 100644 index 0000000000..26674d6eb5 --- /dev/null +++ b/src/Resources/Grammars/ocaml.json @@ -0,0 +1,2961 @@ +{ + "name": "ocaml", + "scopeName": "source.ocaml", + "fileTypes": [".ml", ".mli"], + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#decl" + } + ], + "repository": { + "attribute": { + "begin": "(\\[)[[:space:]]*((?|~$])@{1,3}(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\]", + "beginCaptures": { + "1": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + "2": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + }, + "patterns": [ + { + "include": "#attributePayload" + } + ] + }, + "attributeIdentifier": { + "match": "((?|~$])%(?![#\\-:!?.@*/&%^+<=>|~$]))((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))", + "captures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "punctuation.definition.tag" + } + } + }, + "attributePayload": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]%|^%))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "((?|~$])[:\\?](?![#\\-:!?.@*/&%^+<=>|~$]))|(?<=[[:space:]])|(?=\\])", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#pathModuleExtended" + }, + { + "include": "#pathRecord" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=\\])", + "patterns": [ + { + "include": "#signature" + }, + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]\\?|^\\?))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=\\])", + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]\\?|^\\?))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=\\])|\\bwhen\\b", + "endCaptures": { + "1": {} + }, + "patterns": [ + { + "include": "#pattern" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]when|^when))(?![[:word:]]))", + "end": "(?=\\])", + "patterns": [ + { + "include": "#term" + } + ] + } + ] + }, + { + "include": "#term" + } + ] + }, + "bindClassTerm": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]class|^class|[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?|~$])(:)|(=)(?![#\\-:!?.@*/&%^+<=>|~$])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]class|^class|[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?=(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)[[:space:]]*,|[^[:space:][:lower:]%])|(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)|(?=\\btype\\b)", + "endCaptures": { + "0": { + "name": "entity.name.function strong emphasis" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + } + ] + }, + { + "begin": "\\[", + "end": "\\]", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "include": "#bindTermArgs" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$])|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|val)\\b)", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#literalClassType" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\band\\b|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + } + ] + }, + "bindClassType": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]class|^class|[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?|~$])(:)|(=)(?![#\\-:!?.@*/&%^+<=>|~$])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]class|^class|[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?=(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)[[:space:]]*,|[^[:space:][:lower:]%])|(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)|(?=\\btype\\b)", + "endCaptures": { + "0": { + "name": "entity.name.function strong emphasis" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + } + ] + }, + { + "begin": "\\[", + "end": "\\]", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "include": "#bindTermArgs" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$])|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|val)\\b)", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#literalClassType" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\band\\b|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "include": "#literalClassType" + } + ] + } + ] + }, + "bindConstructor": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]exception|^exception))(?![[:word:]]))|(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]\\+=|^\\+=|[^#\\-:!?.@*/&%^+<=>|~$]=|^=|[^#\\-:!?.@*/&%^+<=>|~$]\\||^\\|))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(:)|(\\bof\\b)|((?|~$])\\|(?![#\\-:!?.@*/&%^+<=>|~$]))|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "punctuation.definition.tag" + }, + "3": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "match": "\\.\\.", + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + { + "match": "\\b(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)\\b(?![[:space:]]*(?:\\.|\\([^\\*]))", + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?:(?<=(?:[^[:word:]]of|^of))(?![[:word:]]))", + "end": "(?|~$])\\|(?![#\\-:!?.@*/&%^+<=>|~$])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + } + ] + }, + "bindSignature": { + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?:(?<=(?:[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$])", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pathModuleExtended" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\band\\b|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "include": "#signature" + } + ] + } + ] + }, + "bindStructure": { + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and))(?![[:word:]]))|(?=[[:upper:]])", + "end": "(?|~$])(:(?!=))|(:?=)(?![#\\-:!?.@*/&%^+<=>|~$])|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "match": "\\bmodule\\b", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + }, + { + "match": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)", + "name": "entity.name.function strong emphasis" + }, + { + "begin": "\\((?!\\))", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?|~$]):(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + } + }, + "patterns": [ + { + "include": "#signature" + } + ] + }, + { + "include": "#variableModule" + } + ] + }, + { + "include": "#literalUnit" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\b(and)\\b|((?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$]))|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#signature" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:=|^:=|[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\b(?:(and)|(with))\\b|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + }, + "2": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "include": "#structure" + } + ] + } + ] + }, + "bindTerm": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]!|^!))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]external|^external|[^[:word:]]let|^let|[^[:word:]]method|^method|[^[:word:]]val|^val))(?![[:word:]]))", + "end": "(\\bmodule\\b)|(\\bopen\\b)|(?|~$])(:)|((?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$]))(?![#\\-:!?.@*/&%^+<=>|~$])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "1": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + }, + "2": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "4": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]!|^!))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]external|^external|[^[:word:]]let|^let|[^[:word:]]method|^method|[^[:word:]]val|^val))(?![[:word:]]))", + "end": "(?=\\b(?:module|open)\\b)|(?=(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)[[:space:]]*,|[^[:space:][:lower:]%])|(\\brec\\b)|((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "2": { + "name": "entity.name.function strong emphasis" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#comment" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]rec|^rec))(?![[:word:]]))", + "end": "((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))|(?=[^[:space:][:alpha:]])", + "endCaptures": { + "0": { + "name": "entity.name.function strong emphasis" + } + }, + "patterns": [ + { + "include": "#bindTermArgs" + } + ] + }, + { + "include": "#bindTermArgs" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]module|^module))(?![[:word:]]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#declModule" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]open|^open))(?![[:word:]]))", + "end": "(?=\\bin\\b)|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#pathModuleSimple" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\btype\\b|(?=[^[:space:]])", + "endCaptures": { + "0": { + "name": "keyword.control" + } + } + }, + { + "begin": "(?:(?<=(?:[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?|~$])\\.(?![#\\-:!?.@*/&%^+<=>|~$])", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#pattern" + } + ] + }, + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\band\\b|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + } + ] + }, + "bindTermArgs": { + "patterns": [ + { + "begin": "~|\\?", + "end": ":|(?=[^[:space:]])", + "applyEndPatternLast": true, + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]~|^~|[^#\\-:!?.@*/&%^+<=>|~$]\\?|^\\?))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)|(?<=\\))", + "endCaptures": { + "0": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "\\((?!\\*)", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "begin": "(?<=\\()", + "end": ":|=", + "endCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + ] + }, + { + "begin": "(?<=:)", + "end": "=|(?=\\))", + "endCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=\\))", + "patterns": [ + { + "include": "#term" + } + ] + } + ] + } + ] + } + ] + }, + { + "include": "#pattern" + } + ] + }, + "bindType": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?|~$])\\+=|=(?![#\\-:!?.@*/&%^+<=>|~$])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#pathType" + }, + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "entity.name.function strong" + }, + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]\\+=|^\\+=|[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\band\\b|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "include": "#bindConstructor" + } + ] + } + ] + }, + "comment": { + "patterns": [ + { + "include": "#attribute" + }, + { + "include": "#extension" + }, + { + "include": "#commentBlock" + }, + { + "include": "#commentDoc" + } + ] + }, + "commentBlock": { + "begin": "\\(\\*(?!\\*[^\\)])", + "end": "\\*\\)", + "name": "comment", + "contentName": "emphasis", + "patterns": [ + { + "include": "#commentBlock" + }, + { + "include": "#commentDoc" + } + ] + }, + "commentDoc": { + "begin": "\\(\\*\\*", + "end": "\\*\\)", + "name": "comment", + "patterns": [ + { + "match": "\\*" + }, + { + "include": "#comment" + } + ] + }, + "decl": { + "patterns": [ + { + "include": "#declClass" + }, + { + "include": "#declException" + }, + { + "include": "#declInclude" + }, + { + "include": "#declModule" + }, + { + "include": "#declOpen" + }, + { + "include": "#declTerm" + }, + { + "include": "#declType" + } + ] + }, + "declClass": { + "begin": "\\bclass\\b", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "entity.name.class constant.numeric markup.underline" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "begin": "(?:(?<=(?:[^[:word:]]class|^class))(?![[:word:]]))", + "end": "\\btype\\b|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|val)\\b)", + "beginCaptures": { + "0": { + "name": "entity.name.class constant.numeric markup.underline" + } + }, + "endCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "include": "#bindClassTerm" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#bindClassType" + } + ] + } + ] + }, + "declException": { + "begin": "\\bexception\\b", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "keyword markup.underline" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#bindConstructor" + } + ] + }, + "declInclude": { + "begin": "\\binclude\\b", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#signature" + } + ] + }, + "declModule": { + "begin": "(?:(?<=(?:[^[:word:]]module|^module))(?![[:word:]]))|\\bmodule\\b", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename markup.underline" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "begin": "(?:(?<=(?:[^[:word:]]module|^module))(?![[:word:]]))", + "end": "(\\btype\\b)|(?=[[:upper:]])", + "endCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#comment" + }, + { + "match": "\\brec\\b", + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]type|^type))(?![[:word:]]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#bindSignature" + } + ] + }, + { + "begin": "(?=[[:upper:]])", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#bindStructure" + } + ] + } + ] + }, + "declOpen": { + "begin": "\\bopen\\b", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#pathModuleExtended" + } + ] + }, + "declTerm": { + "begin": "\\b(?:(external|val)|(method)|(let))\\b(!?)", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "1": { + "name": "support.type markup.underline" + }, + "2": { + "name": "storage.type markup.underline" + }, + "3": { + "name": "keyword.control markup.underline" + }, + "4": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#bindTerm" + } + ] + }, + "declType": { + "begin": "(?:(?<=(?:[^[:word:]]type|^type))(?![[:word:]]))|\\btype\\b", + "end": ";;|(?=\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "keyword markup.underline" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#bindType" + } + ] + }, + "extension": { + "begin": "(\\[)((?|~$])%{1,3}(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\]", + "beginCaptures": { + "1": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + "2": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + }, + "patterns": [ + { + "include": "#attributePayload" + } + ] + }, + "literal": { + "patterns": [ + { + "include": "#termConstructor" + }, + { + "include": "#literalArray" + }, + { + "include": "#literalBoolean" + }, + { + "include": "#literalCharacter" + }, + { + "include": "#literalList" + }, + { + "include": "#literalNumber" + }, + { + "include": "#literalObjectTerm" + }, + { + "include": "#literalString" + }, + { + "include": "#literalRecord" + }, + { + "include": "#literalUnit" + } + ] + }, + "literalArray": { + "begin": "\\[\\|", + "end": "\\|\\]", + "captures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + "literalBoolean": { + "match": "\\bfalse|true\\b", + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + "literalCharacter": { + "begin": "(?|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]with|^with))(?![[:word:]]))", + "end": "(:)|(=)|(;)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(;)|(=)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": ";|(?=\\})", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + } + ] + }, + "literalString": { + "patterns": [ + { + "begin": "\"", + "end": "\"", + "name": "string beginning.punctuation.definition.quote.markdown", + "patterns": [ + { + "include": "#literalStringEscape" + } + ] + }, + { + "begin": "(\\{)([_[:lower:]]*?)(\\|)", + "end": "(\\|)(\\2)(\\})", + "name": "string beginning.punctuation.definition.quote.markdown", + "patterns": [ + { + "include": "#literalStringEscape" + } + ] + } + ] + }, + "literalStringEscape": { + "match": "\\\\(?:[\\\\\"ntbr]|[[:digit:]][[:digit:]][[:digit:]]|x[[:xdigit:]][[:xdigit:]]|o[0-3][0-7][0-7])" + }, + "literalUnit": { + "match": "\\(\\)", + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + "pathModuleExtended": { + "patterns": [ + { + "include": "#pathModulePrefixExtended" + }, + { + "match": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)", + "name": "entity.name.class constant.numeric" + } + ] + }, + "pathModulePrefixExtended": { + "begin": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\.|$|\\()", + "end": "(?![[:space:]\\.]|$|\\()", + "beginCaptures": { + "0": { + "name": "entity.name.class constant.numeric" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "\\(", + "end": "\\)", + "captures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "match": "((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\)))", + "name": "string.other.link variable.language variable.parameter emphasis" + }, + { + "include": "#structure" + } + ] + }, + { + "begin": "(?|~$])\\.(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\.|$))|((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*(?:$|\\()))|((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\)))|(?![[:space:]\\.[:upper:]]|$|\\()", + "beginCaptures": { + "0": { + "name": "keyword strong" + } + }, + "endCaptures": { + "1": { + "name": "entity.name.class constant.numeric" + }, + "2": { + "name": "entity.name.function strong" + }, + "3": { + "name": "string.other.link variable.language variable.parameter emphasis" + } + } + } + ] + }, + "pathModulePrefixExtendedParens": { + "begin": "\\(", + "end": "\\)", + "captures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "match": "((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\)))", + "name": "string.other.link variable.language variable.parameter emphasis" + }, + { + "include": "#structure" + } + ] + }, + "pathModulePrefixSimple": { + "begin": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\.)", + "end": "(?![[:space:]\\.])", + "beginCaptures": { + "0": { + "name": "entity.name.class constant.numeric" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?|~$])\\.(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*\\.))|((?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)(?=[[:space:]]*))|(?![[:space:]\\.[:upper:]])", + "beginCaptures": { + "0": { + "name": "keyword strong" + } + }, + "endCaptures": { + "1": { + "name": "entity.name.class constant.numeric" + }, + "2": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + } + } + ] + }, + "pathModuleSimple": { + "patterns": [ + { + "include": "#pathModulePrefixSimple" + }, + { + "match": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)", + "name": "entity.name.class constant.numeric" + } + ] + }, + "pathRecord": { + "patterns": [ + { + "begin": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "end": "(?=[^[:space:]\\.])(?!\\(\\*)", + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]\\.|^\\.))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?|~$])\\.(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "((?|~$])\\.(?![#\\-:!?.@*/&%^+<=>|~$]))|((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|mutable|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))|(?<=\\))|(?<=\\])", + "beginCaptures": { + "0": { + "name": "keyword strong" + } + }, + "endCaptures": { + "1": { + "name": "keyword strong" + }, + "2": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pathModulePrefixSimple" + }, + { + "begin": "\\((?!\\*)", + "end": "\\)", + "captures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + { + "begin": "\\[", + "end": "\\]", + "captures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#pattern" + } + ] + } + ] + } + ] + } + ] + }, + "pattern": { + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#patternArray" + }, + { + "include": "#patternLazy" + }, + { + "include": "#patternList" + }, + { + "include": "#patternMisc" + }, + { + "include": "#patternModule" + }, + { + "include": "#patternRecord" + }, + { + "include": "#literal" + }, + { + "include": "#patternParens" + }, + { + "include": "#patternType" + }, + { + "include": "#variablePattern" + }, + { + "include": "#termOperator" + } + ] + }, + "patternArray": { + "begin": "\\[\\|", + "end": "\\|\\]", + "captures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + }, + "patterns": [ + { + "include": "#pattern" + } + ] + }, + "patternLazy": { + "match": "lazy", + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "patternList": { + "begin": "\\[", + "end": "\\]", + "captures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + }, + "patterns": [ + { + "include": "#pattern" + } + ] + }, + "patternMisc": { + "match": "((?|~$]),(?![#\\-:!?.@*/&%^+<=>|~$]))|([#\\-:!?.@*/&%^+<=>|~$]+)|\\b(as)\\b", + "captures": { + "1": { + "name": "string.regexp strong" + }, + "2": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + } + }, + "patternModule": { + "begin": "\\bmodule\\b", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + }, + "patterns": [ + { + "include": "#declModule" + } + ] + }, + "patternParens": { + "begin": "\\((?!\\))", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?|~$]):(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "include": "#pattern" + } + ] + }, + "patternRecord": { + "begin": "\\{", + "end": "\\}", + "captures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong strong" + } + }, + "patterns": [ + { + "begin": "(?<=\\{|;)", + "end": "(:)|(=)|(;)|(with)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "4": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pathModulePrefixSimple" + }, + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]with|^with))(?![[:word:]]))", + "end": "(:)|(=)|(;)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(;)|(=)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": ";|(?=\\})", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#pattern" + } + ] + } + ] + }, + "patternType": { + "begin": "\\btype\\b", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "include": "#declType" + } + ] + }, + "pragma": { + "begin": "(?|~$])#(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#literalNumber" + }, + { + "include": "#literalString" + } + ] + }, + "signature": { + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#signatureLiteral" + }, + { + "include": "#signatureFunctor" + }, + { + "include": "#pathModuleExtended" + }, + { + "include": "#signatureParens" + }, + { + "include": "#signatureRecovered" + }, + { + "include": "#signatureConstraints" + } + ] + }, + "signatureConstraints": { + "begin": "\\bwith\\b", + "end": "(?=\\))|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]with|^with))(?![[:word:]]))", + "end": "\\b(?:(module)|(type))\\b", + "endCaptures": { + "1": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + }, + "2": { + "name": "keyword" + } + } + }, + { + "include": "#declModule" + }, + { + "include": "#declType" + } + ] + }, + "signatureFunctor": { + "patterns": [ + { + "begin": "\\bfunctor\\b", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]functor|^functor))(?![[:word:]]))", + "end": "(\\(\\))|(\\((?!\\)))", + "endCaptures": { + "1": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + "2": { + "name": "punctuation.definition.tag" + } + } + }, + { + "begin": "(?<=\\()", + "end": "(:)|(\\))", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#variableModule" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#signature" + } + ] + }, + { + "begin": "(?<=\\))", + "end": "(\\()|((?|~$])->(?![#\\-:!?.@*/&%^+<=>|~$]))", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag" + }, + "2": { + "name": "support.type strong" + } + } + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]->|^->))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#signature" + } + ] + } + ] + }, + { + "match": "(?|~$])->(?![#\\-:!?.@*/&%^+<=>|~$])", + "name": "support.type strong" + } + ] + }, + "signatureLiteral": { + "begin": "\\bsig\\b", + "end": "\\bend\\b", + "captures": { + "0": { + "name": "punctuation.definition.tag emphasis" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#decl" + } + ] + }, + "signatureParens": { + "begin": "\\((?!\\))", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "(?|~$]):(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + } + }, + "patterns": [ + { + "include": "#signature" + } + ] + }, + { + "include": "#signature" + } + ] + }, + "signatureRecovered": { + "patterns": [ + { + "begin": "\\(|(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:|[^#\\-:!?.@*/&%^+<=>|~$]->|^->))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?:(?<=(?:[^[:word:]]include|^include|[^[:word:]]open|^open))(?![[:word:]]))", + "end": "\\bmodule\\b|(?!$|[[:space:]]|\\bmodule\\b)", + "endCaptures": { + "0": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + } + }, + { + "begin": "(?:(?<=(?:[^[:word:]]module|^module))(?![[:word:]]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]module|^module))(?![[:word:]]))", + "end": "\\btype\\b", + "endCaptures": { + "0": { + "name": "keyword" + } + } + }, + { + "begin": "(?:(?<=(?:[^[:word:]]type|^type))(?![[:word:]]))", + "end": "\\bof\\b", + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + } + }, + { + "begin": "(?:(?<=(?:[^[:word:]]of|^of))(?![[:word:]]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#signature" + } + ] + } + ] + } + ] + }, + "structure": { + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#structureLiteral" + }, + { + "include": "#structureFunctor" + }, + { + "include": "#pathModuleExtended" + }, + { + "include": "#structureParens" + } + ] + }, + "structureFunctor": { + "patterns": [ + { + "begin": "\\bfunctor\\b", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]functor|^functor))(?![[:word:]]))", + "end": "(\\(\\))|(\\((?!\\)))", + "endCaptures": { + "1": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + }, + "2": { + "name": "punctuation.definition.tag" + } + } + }, + { + "begin": "(?<=\\()", + "end": "(:)|(\\))", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#variableModule" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#signature" + } + ] + }, + { + "begin": "(?<=\\))", + "end": "(\\()|((?|~$])->(?![#\\-:!?.@*/&%^+<=>|~$]))", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag" + }, + "2": { + "name": "support.type strong" + } + } + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]->|^->))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "patterns": [ + { + "include": "#structure" + } + ] + } + ] + }, + { + "match": "(?|~$])->(?![#\\-:!?.@*/&%^+<=>|~$])", + "name": "support.type strong" + } + ] + }, + "structureLiteral": { + "begin": "\\bstruct\\b", + "end": "\\bend\\b", + "captures": { + "0": { + "name": "punctuation.definition.tag emphasis" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pragma" + }, + { + "include": "#decl" + } + ] + }, + "structureParens": { + "begin": "\\(", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#structureUnpack" + }, + { + "include": "#structure" + } + ] + }, + "structureUnpack": { + "begin": "\\bval\\b", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + } + }, + "term": { + "patterns": [ + { + "include": "#termLet" + }, + { + "include": "#termAtomic" + } + ] + }, + "termAtomic": { + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#termConditional" + }, + { + "include": "#termConstructor" + }, + { + "include": "#termDelim" + }, + { + "include": "#termFor" + }, + { + "include": "#termFunction" + }, + { + "include": "#literal" + }, + { + "include": "#termMatch" + }, + { + "include": "#termMatchRule" + }, + { + "include": "#termPun" + }, + { + "include": "#termOperator" + }, + { + "include": "#termTry" + }, + { + "include": "#termWhile" + }, + { + "include": "#pathRecord" + } + ] + }, + "termConditional": { + "match": "\\b(?:if|then|else)\\b", + "name": "keyword.control" + }, + "termConstructor": { + "patterns": [ + { + "include": "#pathModulePrefixSimple" + }, + { + "match": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)", + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong" + } + ] + }, + "termDelim": { + "patterns": [ + { + "begin": "\\((?!\\))", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + { + "begin": "\\bbegin\\b", + "end": "\\bend\\b", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "include": "#attributeIdentifier" + }, + { + "include": "#term" + } + ] + } + ] + }, + "termFor": { + "patterns": [ + { + "begin": "\\bfor\\b", + "end": "\\bdone\\b", + "beginCaptures": { + "0": { + "name": "keyword.control" + } + }, + "endCaptures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]for|^for))(?![[:word:]]))", + "end": "(?|~$])=(?![#\\-:!?.@*/&%^+<=>|~$])", + "endCaptures": { + "0": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#pattern" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "\\b(?:downto|to)\\b", + "endCaptures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]to|^to))(?![[:word:]]))", + "end": "\\bdo\\b", + "endCaptures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]do|^do))(?![[:word:]]))", + "end": "(?=\\bdone\\b)", + "patterns": [ + { + "include": "#term" + } + ] + } + ] + } + ] + }, + "termFunction": { + "match": "\\b(?:(fun)|(function))\\b", + "captures": { + "1": { + "name": "storage.type" + }, + "2": { + "name": "storage.type" + } + } + }, + "termLet": { + "patterns": [ + { + "begin": "(?:(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=|[^#\\-:!?.@*/&%^+<=>|~$]->|^->))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?<=;|\\())(?=[[:space:]]|\\blet\\b)|(?:(?<=(?:[^[:word:]]begin|^begin|[^[:word:]]do|^do|[^[:word:]]else|^else|[^[:word:]]in|^in|[^[:word:]]struct|^struct|[^[:word:]]then|^then|[^[:word:]]try|^try))(?![[:word:]]))|(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]@@|^@@))(?![#\\-:!?.@*/&%^+<=>|~$]))[[:space:]]+", + "end": "\\b(?:(and)|(let))\\b|(?=[^[:space:]])(?!\\(\\*)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + }, + "2": { + "name": "storage.type markup.underline" + } + }, + "patterns": [ + { + "include": "#comment" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]and|^and|[^[:word:]]let|^let))(?![[:word:]]))|(let)", + "end": "\\b(?:(and)|(in))\\b|(?=\\}|\\)|\\]|\\b(?:end|class|exception|external|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "1": { + "name": "storage.type markup.underline" + } + }, + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp markup.underline" + }, + "2": { + "name": "storage.type markup.underline" + } + }, + "patterns": [ + { + "include": "#bindTerm" + } + ] + } + ] + }, + "termMatch": { + "begin": "\\bmatch\\b", + "end": "\\bwith\\b", + "captures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + "termMatchRule": { + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]fun|^fun|[^[:word:]]function|^function|[^[:word:]]with|^with))(?![[:word:]]))", + "end": "(?|~$])(\\|)|(->)(?![#\\-:!?.@*/&%^+<=>|~$])", + "endCaptures": { + "1": { + "name": "support.type strong" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#attributeIdentifier" + }, + { + "include": "#pattern" + } + ] + }, + { + "begin": "(?:(?<=(?:[^\\[#\\-:!?.@*/&%^+<=>|~$]\\||^\\|))(?![#\\-:!?.@*/&%^+<=>|~$]))|(?|~$])\\|(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "(?|~$])(\\|)|(->)(?![#\\-:!?.@*/&%^+<=>|~$])", + "beginCaptures": { + "0": { + "name": "support.type strong" + } + }, + "endCaptures": { + "1": { + "name": "support.type strong" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#pattern" + }, + { + "begin": "\\bwhen\\b", + "end": "(?=(?|~$])->(?![#\\-:!?.@*/&%^+<=>|~$]))", + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + } + ] + } + ] + }, + "termOperator": { + "patterns": [ + { + "begin": "(?|~$])#(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "beginCaptures": { + "0": { + "name": "keyword" + } + }, + "endCaptures": { + "0": { + "name": "entity.name.function" + } + } + }, + { + "match": "<-", + "captures": { + "0": { + "name": "keyword.control strong" + } + } + }, + { + "match": "(,|[#\\-:!?.@*/&%^+<=>|~$]+)|(;)", + "captures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "2": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + } + }, + { + "match": "\\b(?:and|assert|asr|land|lazy|lsr|lxor|mod|new|or)\\b", + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + ] + }, + "termPun": { + "begin": "(?|~$])\\?|~(?![#\\-:!?.@*/&%^+<=>|~$])", + "end": ":|(?=[^[:space:]:])", + "applyEndPatternLast": true, + "beginCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "endCaptures": { + "0": { + "name": "keyword" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]\\?|^\\?|[^#\\-:!?.@*/&%^+<=>|~$]~|^~))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "endCaptures": { + "0": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + } + } + ] + }, + "termTry": { + "begin": "\\btry\\b", + "end": "\\bwith\\b", + "captures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + "termWhile": { + "patterns": [ + { + "begin": "\\bwhile\\b", + "end": "\\bdone\\b", + "beginCaptures": { + "0": { + "name": "keyword.control" + } + }, + "endCaptures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "begin": "(?:(?<=(?:[^[:word:]]while|^while))(?![[:word:]]))", + "end": "\\bdo\\b", + "endCaptures": { + "0": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#term" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]do|^do))(?![[:word:]]))", + "end": "(?=\\bdone\\b)", + "patterns": [ + { + "include": "#term" + } + ] + } + ] + } + ] + }, + "type": { + "patterns": [ + { + "include": "#comment" + }, + { + "match": "\\bnonrec\\b", + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + { + "include": "#pathModulePrefixExtended" + }, + { + "include": "#typeLabel" + }, + { + "include": "#typeObject" + }, + { + "include": "#typeOperator" + }, + { + "include": "#typeParens" + }, + { + "include": "#typePolymorphicVariant" + }, + { + "include": "#typeRecord" + }, + { + "include": "#typeConstructor" + } + ] + }, + "typeConstructor": { + "patterns": [ + { + "begin": "(_)|((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))|(')((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))|(?<=[^\\*]\\)|\\])", + "end": "(?=\\((?!\\*)|\\*|:|,|=|\\.|>|-|\\{|\\[|\\+|\\}|\\)|\\]|;|\\|)|((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))[:space:]*(?!\\(\\*|[[:word:]])|(?=;;|\\}|\\)|\\]|\\b(?:end|and|class|exception|external|in|include|inherit|initializer|let|method|module|open|type|val)\\b)", + "beginCaptures": { + "1": { + "name": "comment constant.regexp meta.separator.markdown" + }, + "3": { + "name": "string.other.link variable.language variable.parameter emphasis strong emphasis" + }, + "4": { + "name": "keyword.control emphasis" + } + }, + "endCaptures": { + "1": { + "name": "entity.name.function strong" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pathModulePrefixExtended" + } + ] + } + ] + }, + "typeLabel": { + "patterns": [ + { + "begin": "(\\??)((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))[[:space:]]*((?|~$]):(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(?=(?|~$])->(?![#\\-:!?.@*/&%^+<=>|~$]))", + "captures": { + "1": { + "name": "keyword strong emphasis" + }, + "2": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + }, + "3": { + "name": "keyword" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + } + ] + }, + "typeModule": { + "begin": "\\bmodule\\b", + "end": "(?=\\))", + "beginCaptures": { + "0": { + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename" + } + }, + "patterns": [ + { + "include": "#pathModuleExtended" + }, + { + "include": "#signatureConstraints" + } + ] + }, + "typeObject": { + "begin": "<", + "end": ">", + "captures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong strong" + } + }, + "patterns": [ + { + "begin": "(?<=<|;)", + "end": "(:)|(?=>)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "4": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pathModulePrefixSimple" + }, + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(;)|(?=>)", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + } + ] + }, + "typeOperator": { + "patterns": [ + { + "match": ",|;|[#\\-:!?.@*/&%^+<=>|~$]+", + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + } + ] + }, + "typeParens": { + "begin": "\\(", + "end": "\\)", + "captures": { + "0": { + "name": "punctuation.definition.tag" + } + }, + "patterns": [ + { + "match": ",", + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + { + "include": "#typeModule" + }, + { + "include": "#type" + } + ] + }, + "typePolymorphicVariant": { + "begin": "\\[", + "end": "\\]", + "patterns": [] + }, + "typeRecord": { + "begin": "\\{", + "end": "\\}", + "captures": { + "0": { + "name": "constant.language constant.numeric entity.other.attribute-name.id.css strong strong" + } + }, + "patterns": [ + { + "begin": "(?<=\\{|;)", + "end": "(:)|(=)|(;)|(with)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "4": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#pathModulePrefixSimple" + }, + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^[:word:]]with|^with))(?![[:word:]]))", + "end": "(:)|(=)|(;)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp strong" + }, + "2": { + "name": "support.type strong" + }, + "3": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "match": "(?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*)", + "name": "markup.inserted constant.language support.constant.property-value entity.name.filename emphasis" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]:|^:))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": "(;)|(=)|(?=\\})", + "endCaptures": { + "1": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + }, + "2": { + "name": "support.type strong" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + }, + { + "begin": "(?:(?<=(?:[^#\\-:!?.@*/&%^+<=>|~$]=|^=))(?![#\\-:!?.@*/&%^+<=>|~$]))", + "end": ";|(?=\\})", + "endCaptures": { + "0": { + "name": "variable.other.class.js message.error variable.interpolation string.regexp" + } + }, + "patterns": [ + { + "include": "#type" + } + ] + } + ] + }, + "variableModule": { + "match": "(?:\\b(?=[[:upper:]])[[:alpha:]_][[:word:]']*)", + "captures": { + "0": { + "name": "string.other.link variable.language variable.parameter emphasis" + } + } + }, + "variablePattern": { + "match": "(\\b_\\b)|((?:(?!\\b(?:and|'|as|asr|assert|\\*|begin|class|:|,|@|constraint|do|done|downto|else|end|=|exception|external|false|for|\\.|fun|function|functor|>|-|if|in|include|inherit|initializer|land|lazy|\\{|\\(|\\[|<|let|lor|lsl|lsr|lxor|match|method|mod|module|mutable|new|nonrec|#|object|of|open|or|%|\\+|private|\\?|\"|rec|\\\\|\\}|\\)|\\]|;|sig|/|struct|then|~|to|true|try|type|val|\\||virtual|when|while|with)\\b(?:[^']|$))\\b(?=[[:lower:]_])[[:alpha:]_][[:word:]']*))", + "captures": { + "1": { + "name": "comment constant.regexp meta.separator.markdown" + }, + "2": { + "name": "string.other.link variable.language variable.parameter emphasis" + } + } + } + } +} diff --git a/src/Resources/Grammars/ocamllex.json b/src/Resources/Grammars/ocamllex.json new file mode 100644 index 0000000000..80795ad9e0 --- /dev/null +++ b/src/Resources/Grammars/ocamllex.json @@ -0,0 +1,302 @@ +{ + "name": "ocamllex", + "scopeName": "source.ocamllex", + "fileTypes": [ "mll" ], + "foldingStartMarker": "{", + "foldingStopMarker": "}", + "keyEquivalent": "^~O", + "patterns": [ + { + "begin": "^\\s*({)", + "beginCaptures": { + "1": { + "name": "punctuation.section.embedded.ocaml.begin.ocamllex" + } + }, + "end": "^\\s*(})", + "endCaptures": { + "1": { + "name": "punctuation.section.embedded.ocaml.end.ocamllex" + } + }, + "name": "meta.embedded.ocaml", + "patterns": [ + { + "include": "source.ocaml" + } + ] + }, + { + "begin": "\\b(let)\\s+([a-z][a-zA-Z0-9'_]*)\\s+=", + "beginCaptures": { + "1": { + "name": "keyword.other.pattern-definition.ocamllex" + }, + "2": { + "name": "entity.name.type.pattern.stupid-goddamn-hack.ocamllex" + } + }, + "end": "^(?:\\s*let)|(?:\\s*(rule|$))", + "name": "meta.pattern-definition.ocaml", + "patterns": [ + { + "include": "#match-patterns" + } + ] + }, + { + "begin": "(rule|and)\\s+([a-z][a-zA-Z0-9_]*)\\s+(=)\\s+(parse)(?=\\s*$)|((?", + "endCaptures": { + "0": { + "name": "punctuation.definition.type-declaration.end.ocamlyacc" + } + }, + "name": "meta.token.type-declaration.ocamlyacc", + "patterns": [ + { + "include": "source.ocaml" + } + ] + } + ] + } + } +} diff --git a/src/Resources/Grammars/swift.json b/src/Resources/Grammars/swift.json new file mode 100644 index 0000000000..c5629f30a7 --- /dev/null +++ b/src/Resources/Grammars/swift.json @@ -0,0 +1,3253 @@ +{ + "comment": "See swift.tmbundle/grammar-test.swift for test cases.", + "fileTypes": ["swift"], + "firstLineMatch": "^#!/.*\\bswift", + "keyEquivalent": "^~S", + "scopeName": "source.swift", + "uuid": "8C743E89-9C8D-4833-8149-6500D82AA463", + "name": "Swift", + "patterns": [{ "include": "#root" }], + + "repository": { + "async-throws": { + "match": "\\b(?:(throws\\s+async|rethrows\\s+async)|(throws|rethrows)|(async))\\b", + "captures": { + "1": { "name": "invalid.illegal.await-must-precede-throws.swift" }, + "2": { "name": "storage.modifier.exception.swift" }, + "3": { "name": "storage.modifier.async.swift" } + } + }, + + "attributes": { + "patterns": [ + { + "name": "meta.attribute.available.swift", + "begin": "((@)available)(\\()", + "end": "\\)", + "beginCaptures": { + "1": { "name": "storage.modifier.attribute.swift" }, + "2": { "name": "punctuation.definition.attribute.swift" }, + "3": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [ + { + "match": "\\b(swift|(?:iOS|macOS|OSX|watchOS|tvOS|visionOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))?", + "captures": { + "1": { "name": "keyword.other.platform.os.swift" }, + "2": { "name": "constant.numeric.swift" } + } + }, + + { + "begin": "\\b(introduced|deprecated|obsoleted)\\s*(:)\\s*", + "end": "(?!\\G)", + "beginCaptures": { + "1": { "name": "keyword.other.swift" }, + "2": { "name": "punctuation.separator.key-value.swift" } + }, + "patterns": [ + { + "name": "constant.numeric.swift", + "match": "\\b[0-9]+(?:\\.[0-9]+)*\\b" + } + ] + }, + + { + "begin": "\\b(message|renamed)\\s*(:)\\s*(?=\")", + "end": "(?!\\G)", + "beginCaptures": { + "1": { "name": "keyword.other.swift" }, + "2": { "name": "punctuation.separator.key-value.swift" } + }, + "patterns": [{ "include": "#literals" }] + }, + + { + "match": "(?:(\\*)|\\b(deprecated|unavailable|noasync)\\b)\\s*(.*?)(?=[,)])", + "captures": { + "1": { "name": "keyword.other.platform.all.swift" }, + "2": { "name": "keyword.other.swift" }, + "3": { "name": "invalid.illegal.character-not-allowed-here.swift" } + } + } + ] + }, + + { + "name": "meta.attribute.objc.swift", + "begin": "((@)objc)(\\()", + "end": "\\)", + "beginCaptures": { + "1": { "name": "storage.modifier.attribute.swift" }, + "2": { "name": "punctuation.definition.attribute.swift" }, + "3": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [ + { + "name": "entity.name.function.swift", + "match": "\\w*(?::(?:\\w*:)*(\\w*))?", + "captures": { + "1": { "name": "invalid.illegal.missing-colon-after-selector-piece.swift" } + } + } + ] + }, + + { + "comment": "any other attribute", + "name": "meta.attribute.swift", + "begin": "(@)(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)", + "end": "(?!\\G\\()", + "beginCaptures": { + "0": { "name": "storage.modifier.attribute.swift" }, + "1": { "name": "punctuation.definition.attribute.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { + "name": "meta.arguments.attribute.swift", + "begin": "\\(", + "end": "\\)", + "beginCaptures": { + "0": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [{ "include": "#expressions" }] + } + ] + } + ] + }, + + "builtin-functions": { + "patterns": [ + { + "comment": "Member functions in the standard library in Swift 3 which may be used with trailing closures and no parentheses", + "name": "support.function.swift", + "match": "(?<=\\.)(?:s(?:ort(?:ed)?|plit)|contains|index|partition|f(?:i(?:lter|rst)|orEach|latMap)|with(?:MutableCharacters|CString|U(?:nsafe(?:Mutable(?:BufferPointer|Pointer(?:s|To(?:Header|Elements)))|BufferPointer)|TF8Buffer))|m(?:in|a(?:p|x)))(?=\\s*[({])\\b" + }, + + { + "comment": "Member functions in the standard library in Swift 3", + "name": "support.function.swift", + "match": "(?<=\\.)(?:s(?:ymmetricDifference|t(?:oreBytes|arts|ride)|ortInPlace|u(?:ccessor|ffix|btract(?:ing|InPlace|WithOverflow)?)|quareRoot|amePosition)|h(?:oldsUnique(?:Reference|OrPinnedReference)|as(?:Suffix|Prefix))|ne(?:gate(?:d)?|xt)|c(?:o(?:untByEnumerating|py(?:Bytes)?)|lamp(?:ed)?|reate)|t(?:o(?:IntMax|Opaque|UIntMax)|ake(?:RetainedValue|UnretainedValue)|r(?:uncatingRemainder|a(?:nscodedLength|ilSurrogate)))|i(?:s(?:MutableAndUniquelyReferenced(?:OrPinned)?|S(?:trictSu(?:perset(?:Of)?|bset(?:Of)?)|u(?:perset(?:Of)?|bset(?:Of)?))|Continuation|T(?:otallyOrdered|railSurrogate)|Disjoint(?:With)?|Unique(?:Reference|lyReferenced(?:OrPinned)?)|Equal|Le(?:ss(?:ThanOrEqualTo)?|adSurrogate))|n(?:sert(?:ContentsOf)?|tersect(?:ion|InPlace)?|itialize(?:Memory|From)?|dex(?:Of|ForKey)))|o(?:verlaps|bjectAt)|d(?:i(?:stance(?:To)?|vide(?:d|WithOverflow)?)|e(?:s(?:cendant|troy)|code(?:CString)?|initialize|alloc(?:ate(?:Capacity)?)?)|rop(?:First|Last))|u(?:n(?:ion(?:InPlace)?|derestimateCount|wrappedOrError)|p(?:date(?:Value)?|percased))|join(?:ed|WithSeparator)|p(?:op(?:First|Last)|ass(?:Retained|Unretained)|re(?:decessor|fix))|e(?:scape(?:d)?|n(?:code|umerate(?:d)?)|lementsEqual|xclusiveOr(?:InPlace)?)|f(?:orm(?:Remainder|S(?:ymmetricDifference|quareRoot)|TruncatingRemainder|In(?:tersection|dex)|Union)|latten|rom(?:CString(?:RepairingIllFormedUTF8)?|Opaque))|w(?:i(?:thMemoryRebound|dth)|rite(?:To)?)|l(?:o(?:wercased|ad)|e(?:adSurrogate|xicographical(?:Compare|lyPrecedes)))|a(?:ss(?:ign(?:BackwardFrom|From)?|umingMemoryBound)|d(?:d(?:ing(?:Product)?|Product|WithOverflow)?|vanced(?:By)?)|utorelease|ppend(?:ContentsOf)?|lloc(?:ate)?|bs)|r(?:ound(?:ed)?|e(?:serveCapacity|tain|duce|place(?:Range|Subrange)?|verse(?:d)?|quest(?:NativeBuffer|UniqueMutableBackingBuffer)|lease|m(?:ove(?:Range|Subrange|Value(?:ForKey)?|First|Last|A(?:tIndex|ll))?|ainder(?:WithOverflow)?)))|ge(?:nerate|t(?:Objects|Element))|m(?:in(?:imum(?:Magnitude)?|Element)|ove(?:Initialize(?:Memory|BackwardFrom|From)?|Assign(?:From)?)?|ultipl(?:y(?:WithOverflow)?|ied)|easure|a(?:ke(?:Iterator|Description)|x(?:imum(?:Magnitude)?|Element)))|bindMemory)(?=\\s*\\()" + }, + + { + "comment": "Member functions in the standard library in Swift 2 only", + "name": "support.function.swift", + "match": "(?<=\\.)(?:s(?:uperclassMirror|amePositionIn|tartsWith)|nextObject|c(?:haracterAtIndex|o(?:untByEnumeratingWithState|pyWithZone)|ustom(?:Mirror|PlaygroundQuickLook))|is(?:EmptyInput|ASCII)|object(?:Enumerator|ForKey|AtIndex)|join|put|keyEnumerator|withUnsafeMutablePointerToValue|length|getMirror|m(?:oveInitializeAssignFrom|ember))(?=\\s*\\()" + } + ] + }, + + "builtin-global-functions": { + "patterns": [ + { + "begin": "\\b(type)(\\()\\s*(of)(:)", + "end": "\\)", + "beginCaptures": { + "1": { "name": "support.function.dynamic-type.swift" }, + "2": { "name": "punctuation.definition.arguments.begin.swift" }, + "3": { "name": "support.variable.parameter.swift" }, + "4": { "name": "punctuation.separator.argument-label.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [{ "include": "#expressions" }] + }, + + { + "comment": "Global functions available in Swift 3 which may be used with trailing closures and no parentheses", + "name": "support.function.swift", + "match": "\\b(?:anyGenerator|autoreleasepool)(?=\\s*[({])\\b" + }, + + { + "comment": "Global functions available in Swift 3", + "name": "support.function.swift", + "match": "\\b(?:s(?:tride(?:of(?:Value)?)?|izeof(?:Value)?|equence|wap)|numericCast|transcode|is(?:UniquelyReferenced(?:NonObjC)?|KnownUniquelyReferenced)|zip|d(?:ump|ebugPrint)|unsafe(?:BitCast|Downcast|Unwrap|Address(?:Of)?)|pr(?:int|econdition(?:Failure)?)|fatalError|with(?:Unsafe(?:MutablePointer|Pointer)|ExtendedLifetime|VaList)|a(?:ssert(?:ionFailure)?|lignof(?:Value)?|bs)|re(?:peatElement|adLine)|getVaList|m(?:in|ax))(?=\\s*\\()" + }, + + { + "comment": "Global functions available in Swift 2 only", + "name": "support.function.swift", + "match": "\\b(?:s(?:ort|uffix|pli(?:ce|t))|insert|overlaps|d(?:istance|rop(?:First|Last))|join|prefix|extend|withUnsafe(?:MutablePointers|Pointers)|lazy|advance|re(?:flect|move(?:Range|Last|A(?:tIndex|ll))))(?=\\s*\\()" + } + ] + }, + + "builtin-properties": { + "patterns": [ + { + "comment": "The simpler (?<=\\bProcess\\.|\\bCommandLine\\.) breaks VS Code / Atom, see https://github.com/textmate/swift.tmbundle/issues/29", + "name": "support.variable.swift", + "match": "(?<=^Process\\.|\\WProcess\\.|^CommandLine\\.|\\WCommandLine\\.)(arguments|argc|unsafeArgv)" + }, + + { + "comment": "Properties in the standard library in Swift 3", + "name": "support.variable.swift", + "match": "(?<=\\.)(?:s(?:t(?:artIndex|ri(?:ngValue|de))|i(?:ze|gn(?:BitIndex|ificand(?:Bit(?:Count|Pattern)|Width)?|alingNaN)?)|u(?:perclassMirror|mmary|bscriptBaseAddress))|h(?:eader|as(?:hValue|PointerRepresentation))|n(?:ulTerminatedUTF8|ext(?:Down|Up)|a(?:n|tiveOwner))|c(?:haracters|ount(?:TrailingZeros)?|ustom(?:Mirror|PlaygroundQuickLook)|apacity)|i(?:s(?:S(?:ign(?:Minus|aling(?:NaN)?)|ubnormal)|N(?:ormal|aN)|Canonical|Infinite|Zero|Empty|Finite|ASCII)|n(?:dices|finity)|dentity)|owner|de(?:scription|bugDescription)|u(?:n(?:safelyUnwrapped|icodeScalar(?:s)?|derestimatedCount)|tf(?:16|8(?:Start|C(?:String|odeUnitCount))?)|intValue|ppercaseString|lp(?:OfOne)?)|p(?:i|ointee)|e(?:ndIndex|lements|xponent(?:Bit(?:Count|Pattern))?)|value(?:s)?|keys|quietNaN|f(?:irst(?:ElementAddress(?:IfContiguous)?)?|loatingPointClass)|l(?:ittleEndian|owercaseString|eastNo(?:nzeroMagnitude|rmalMagnitude)|a(?:st|zy))|a(?:l(?:ignment|l(?:ocatedElementCount|Zeros))|rray(?:PropertyIsNativeTypeChecked)?)|ra(?:dix|wValue)|greatestFiniteMagnitude|m(?:in|emory|ax)|b(?:yteS(?:ize|wapped)|i(?:nade|tPattern|gEndian)|uffer|ase(?:Address)?))\\b" + }, + + { + "comment": "Properties in the standard library in Swift 2 only", + "name": "support.variable.swift", + "match": "(?<=\\.)(?:boolValue|disposition|end|objectIdentifier|quickLookObject|start|valueType)\\b" + }, + + { + "comment": "Enum cases in the standard library - note that there is some overlap between these and the properties", + "name": "support.variable.swift", + "match": "(?<=\\.)(?:s(?:calarValue|i(?:ze|gnalingNaN)|o(?:und|me)|uppressed|prite|et)|n(?:one|egative(?:Subnormal|Normal|Infinity|Zero))|c(?:ol(?:or|lection)|ustomized)|t(?:o(?:NearestOr(?:Even|AwayFromZero)|wardZero)|uple|ext)|i(?:nt|mage)|optional|d(?:ictionary|o(?:uble|wn))|u(?:Int|p|rl)|p(?:o(?:sitive(?:Subnormal|Normal|Infinity|Zero)|int)|lus)|e(?:rror|mptyInput)|view|quietNaN|float|a(?:ttributedString|wayFromZero)|r(?:ectangle|ange)|generated|minus|b(?:ool|ezierPath))\\b" + } + ] + }, + + "builtin-types": { + "comment": "Types provided in the standard library", + "patterns": [ + { "include": "#builtin-types-builtin-class-type" }, + { "include": "#builtin-types-builtin-enum-type" }, + { "include": "#builtin-types-builtin-protocol-type" }, + { "include": "#builtin-types-builtin-struct-type" }, + { "include": "#builtin-types-builtin-typealias" }, + + { + "name": "support.type.any.swift", + "match": "\\bAny\\b" + } + ] + }, + + "builtin-types-builtin-class-type": { + "comment": "Builtin class types", + "name": "support.class.swift", + "match": "\\b(Managed(Buffer|ProtoBuffer)|NonObjectiveCBase|AnyGenerator)\\b" + }, + + "builtin-types-builtin-enum-type": { + "patterns": [ + { + "comment": "CommandLine is an enum, but it acts like a constant", + "name": "support.constant.swift", + "match": "\\b(?:CommandLine|Process(?=\\.))\\b" + }, + + { + "comment": "The return type of a function that never returns", + "name": "support.constant.never.swift", + "match": "\\bNever\\b" + }, + + { + "comment": "Enum types in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:ImplicitlyUnwrappedOptional|Representation|MemoryLayout|FloatingPointClassification|SetIndexRepresentation|SetIteratorRepresentation|FloatingPointRoundingRule|UnicodeDecodingResult|Optional|DictionaryIndexRepresentation|AncestorRepresentation|DisplayStyle|PlaygroundQuickLook|Never|FloatingPointSign|Bit|DictionaryIteratorRepresentation)\\b" + }, + + { + "comment": "Enum types in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:MirrorDisposition|QuickLookObject)\\b" + } + ] + }, + + "builtin-types-builtin-protocol-type": { + "patterns": [ + { + "name": "support.type.swift", + "match": "\\bActor\\b" + }, + + { + "comment": "Protocols in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:Ra(?:n(?:domAccess(?:Collection|Indexable)|geReplaceable(?:Collection|Indexable))|wRepresentable)|M(?:irrorPath|utable(?:Collection|Indexable))|Bi(?:naryFloatingPoint|twiseOperations|directional(?:Collection|Indexable))|S(?:tr(?:ideable|eamable)|igned(?:Number|Integer)|e(?:tAlgebra|quence))|Hashable|C(?:o(?:llection|mparable)|ustom(?:Reflectable|StringConvertible|DebugStringConvertible|PlaygroundQuickLookable|LeafReflectable)|VarArg)|TextOutputStream|I(?:n(?:teger(?:Arithmetic)?|dexable(?:Base)?)|teratorProtocol)|OptionSet|Un(?:signedInteger|icodeCodec)|E(?:quatable|rror|xpressibleBy(?:BooleanLiteral|String(?:Interpolation|Literal)|NilLiteral|IntegerLiteral|DictionaryLiteral|UnicodeScalarLiteral|ExtendedGraphemeClusterLiteral|FloatLiteral|ArrayLiteral))|FloatingPoint|L(?:osslessStringConvertible|azy(?:SequenceProtocol|CollectionProtocol))|A(?:nyObject|bsoluteValuable))\\b" + }, + + { + "comment": "Protocols in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:Ran(?:domAccessIndexType|geReplaceableCollectionType)|GeneratorType|M(?:irror(?:Type|PathType)|utable(?:Sliceable|CollectionType))|B(?:i(?:twiseOperationsType|directionalIndexType)|oolean(?:Type|LiteralConvertible))|S(?:tring(?:InterpolationConvertible|LiteralConvertible)|i(?:nkType|gned(?:NumberType|IntegerType))|e(?:tAlgebraType|quenceType)|liceable)|NilLiteralConvertible|C(?:ollectionType|VarArgType)|Inte(?:rvalType|ger(?:Type|LiteralConvertible|ArithmeticType))|O(?:utputStreamType|ptionSetType)|DictionaryLiteralConvertible|Un(?:signedIntegerType|icode(?:ScalarLiteralConvertible|CodecType))|E(?:rrorType|xten(?:sibleCollectionType|dedGraphemeClusterLiteralConvertible))|F(?:orwardIndexType|loat(?:ingPointType|LiteralConvertible))|A(?:nyCollectionType|rrayLiteralConvertible))\\b" + } + ] + }, + + "builtin-types-builtin-struct-type": { + "patterns": [ + { + "comment": "Structs in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:R(?:e(?:peat(?:ed)?|versed(?:RandomAccess(?:Collection|Index)|Collection|Index))|an(?:domAccessSlice|ge(?:Replaceable(?:RandomAccessSlice|BidirectionalSlice|Slice)|Generator)?))|Generator(?:Sequence|OfOne)|M(?:irror|utable(?:Ran(?:domAccessSlice|geReplaceable(?:RandomAccessSlice|BidirectionalSlice|Slice))|BidirectionalSlice|Slice)|anagedBufferPointer)|B(?:idirectionalSlice|ool)|S(?:t(?:aticString|ri(?:ng|deT(?:hrough(?:Generator|Iterator)?|o(?:Generator|Iterator)?)))|et(?:I(?:ndex|terator))?|lice)|HalfOpenInterval|C(?:haracter(?:View)?|o(?:ntiguousArray|untable(?:Range|ClosedRange)|llectionOfOne)|OpaquePointer|losed(?:Range(?:I(?:ndex|terator))?|Interval)|VaListPointer)|I(?:n(?:t(?:16|8|32|64)?|d(?:ices|ex(?:ing(?:Generator|Iterator))?))|terator(?:Sequence|OverOne)?)|Zip2(?:Sequence|Iterator)|O(?:paquePointer|bjectIdentifier)|D(?:ictionary(?:I(?:ndex|terator)|Literal)?|ouble|efault(?:RandomAccessIndices|BidirectionalIndices|Indices))|U(?:n(?:safe(?:RawPointer|Mutable(?:RawPointer|BufferPointer|Pointer)|BufferPointer(?:Generator|Iterator)?|Pointer)|icodeScalar(?:View)?|foldSequence|managed)|TF(?:16(?:View)?|8(?:View)?|32)|Int(?:16|8|32|64)?)|Join(?:Generator|ed(?:Sequence|Iterator))|PermutationGenerator|E(?:numerate(?:Generator|Sequence|d(?:Sequence|Iterator))|mpty(?:Generator|Collection|Iterator))|Fl(?:oat(?:80)?|atten(?:Generator|BidirectionalCollection(?:Index)?|Sequence|Collection(?:Index)?|Iterator))|L(?:egacyChildren|azy(?:RandomAccessCollection|Map(?:RandomAccessCollection|Generator|BidirectionalCollection|Sequence|Collection|Iterator)|BidirectionalCollection|Sequence|Collection|Filter(?:Generator|BidirectionalCollection|Sequence|Collection|I(?:ndex|terator))))|A(?:ny(?:RandomAccessCollection|Generator|BidirectionalCollection|Sequence|Hashable|Collection|I(?:ndex|terator))|utoreleasingUnsafeMutablePointer|rray(?:Slice)?))\\b" + }, + + { + "comment": "Structs in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:R(?:everse(?:RandomAccess(?:Collection|Index)|Collection|Index)|awByte)|Map(?:Generator|Sequence|Collection)|S(?:inkOf|etGenerator)|Zip2Generator|DictionaryGenerator|Filter(?:Generator|Sequence|Collection(?:Index)?)|LazyForwardCollection|Any(?:RandomAccessIndex|BidirectionalIndex|Forward(?:Collection|Index)))\\b" + } + ] + }, + + "builtin-types-builtin-typealias": { + "patterns": [ + { + "comment": "Typealiases in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:Raw(?:Significand|Exponent|Value)|B(?:ooleanLiteralType|uffer|ase)|S(?:t(?:orage|r(?:i(?:ngLiteralType|de)|eam(?:1|2)))|ubSequence)|NativeBuffer|C(?:hild(?:ren)?|Bool|S(?:hort|ignedChar)|odeUnit|Char(?:16|32)?|Int|Double|Unsigned(?:Short|Char|Int|Long(?:Long)?)|Float|WideChar|Long(?:Long)?)|I(?:n(?:t(?:Max|egerLiteralType)|d(?:ices|ex(?:Distance)?))|terator)|Distance|U(?:n(?:icodeScalar(?:Type|Index|View|LiteralType)|foldFirstSequence)|TF(?:16(?:Index|View)|8Index)|IntMax)|E(?:lement(?:s)?|x(?:tendedGraphemeCluster(?:Type|LiteralType)|ponent))|V(?:oid|alue)|Key|Float(?:32|LiteralType|64)|AnyClass)\\b" + }, + + { + "comment": "Typealiases in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:Generator|PlaygroundQuickLook|UWord|Word)\\b" + } + ] + }, + + "code-block": { + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.section.scope.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.section.scope.end.swift" } + }, + "patterns": [{ "include": "$self" }] + }, + + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.swift", + "match": "\\A^(#!).*$\\n?", + "captures": { + "1": { "name": "punctuation.definition.comment.swift" } + } + }, + + { + "name": "comment.block.documentation.swift", + "begin": "/\\*\\*(?!/)", + "end": "\\*/", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.comment.end.swift" } + }, + "patterns": [{ "include": "#comments-nested" }] + }, + + { + "name": "comment.block.documentation.playground.swift", + "begin": "/\\*:", + "end": "\\*/", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.comment.end.swift" } + }, + "patterns": [{ "include": "#comments-nested" }] + }, + + { + "name": "comment.block.swift", + "begin": "/\\*", + "end": "\\*/", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.comment.end.swift" } + }, + "patterns": [{ "include": "#comments-nested" }] + }, + + { + "name": "invalid.illegal.unexpected-end-of-block-comment.swift", + "match": "\\*/" + }, + + { + "begin": "(^[ \\t]+)?(?=//)", + "end": "(?!\\G)", + "beginCaptures": { + "1": { "name": "punctuation.whitespace.comment.leading.swift" } + }, + "patterns": [ + { + "name": "comment.line.triple-slash.documentation.swift", + "begin": "///", + "end": "$", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.swift" } + } + }, + + { + "name": "comment.line.double-slash.documentation.swift", + "begin": "//:", + "end": "$", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.swift" } + } + }, + + { + "name": "comment.line.double-slash.swift", + "begin": "//", + "end": "$", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.swift" } + } + } + ] + } + ] + }, + + "comments-nested": { + "begin": "/\\*", + "end": "\\*/", + "patterns": [{ "include": "#comments-nested" }] + }, + + "compiler-control": { + "patterns": [ + { + "contentName": "comment.block.preprocessor.swift", + "begin": "^\\s*(#)(if|elseif)\\s+(false)\\b.*?(?=$|//|/\\*)", + "end": "(?=^\\s*(#(elseif|else|endif)\\b))", + "beginCaptures": { + "0": { "name": "meta.preprocessor.conditional.swift" }, + "1": { "name": "punctuation.definition.preprocessor.swift" }, + "2": { "name": "keyword.control.import.preprocessor.conditional.swift" }, + "3": { "name": "constant.language.boolean.swift" } + } + }, + + { + "name": "meta.preprocessor.conditional.swift", + "captures": { + "1": { "name": "punctuation.definition.preprocessor.swift" }, + "2": { "name": "keyword.control.import.preprocessor.conditional.swift" } + }, + "begin": "^\\s*(#)(if|elseif)\\s+", + "end": "(?=\\s*(?://|/\\*))|$", + "patterns": [ + { + "name": "keyword.operator.logical.swift", + "match": "(&&|\\|\\|)" + }, + + { + "name": "constant.language.boolean.swift", + "match": "\\b(true|false)\\b" + }, + + { + "match": "\\b(arch)\\s*(\\()\\s*(?:(arm|arm64|powerpc64|powerpc64le|i386|x86_64|s390x)|\\w+)\\s*(\\))", + "captures": { + "1": { "name": "keyword.other.condition.swift" }, + "2": { "name": "punctuation.definition.parameters.begin.swift" }, + "3": { "name": "support.constant.platform.architecture.swift" }, + "4": { "name": "punctuation.definition.parameters.end.swift" } + } + }, + + { + "match": "\\b(os)\\s*(\\()\\s*(?:(macOS|OSX|iOS|tvOS|watchOS|visionOS|Android|Linux|FreeBSD|Windows|PS4)|\\w+)\\s*(\\))", + "captures": { + "1": { "name": "keyword.other.condition.swift" }, + "2": { "name": "punctuation.definition.parameters.begin.swift" }, + "3": { "name": "support.constant.platform.os.swift" }, + "4": { "name": "punctuation.definition.parameters.end.swift" } + } + }, + + { + "match": "\\b(canImport)\\s*(\\()([\\p{L}_][\\p{L}_\\p{N}\\p{M}]*)(\\))", + "captures": { + "1": { "name": "keyword.other.condition.swift" }, + "2": { "name": "punctuation.definition.parameters.begin.swift" }, + "3": { "name": "entity.name.type.module.swift" }, + "4": { "name": "punctuation.definition.parameters.end.swift" } + } + }, + + { + "begin": "\\b(targetEnvironment)\\s*(\\()", + "end": "(\\))|$", + "beginCaptures": { + "1": { "name": "keyword.other.condition.swift" }, + "2": { "name": "punctuation.definition.parameters.begin.swift" } + }, + "endCaptures": { + "1": { "name": "punctuation.definition.parameters.end.swift" } + }, + "patterns": [ + { + "name": "support.constant.platform.environment.swift", + "match": "\\b(simulator|UIKitForMac)\\b" + } + ] + }, + + { + "begin": "\\b(swift|compiler)\\s*(\\()", + "end": "(\\))|$", + "beginCaptures": { + "1": { "name": "keyword.other.condition.swift" }, + "2": { "name": "punctuation.definition.parameters.begin.swift" } + }, + "endCaptures": { + "1": { "name": "punctuation.definition.parameters.end.swift" } + }, + "patterns": [ + { + "name": "keyword.operator.comparison.swift", + "match": ">=|<" + }, + + { + "name": "constant.numeric.swift", + "match": "\\b[0-9]+(?:\\.[0-9]+)*\\b" + } + ] + } + ] + }, + + { + "name": "meta.preprocessor.conditional.swift", + "match": "^\\s*(#)(else|endif)(.*?)(?=$|//|/\\*)", + "captures": { + "1": { "name": "punctuation.definition.preprocessor.swift" }, + "2": { "name": "keyword.control.import.preprocessor.conditional.swift" }, + "3": { + "patterns": [ + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "\\S+" + } + ] + } + } + }, + + { + "name": "meta.preprocessor.sourcelocation.swift", + "match": "^\\s*(#)(sourceLocation)((\\()([^)]*)(\\)))(.*?)(?=$|//|/\\*)", + "captures": { + "1": { "name": "punctuation.definition.preprocessor.swift" }, + "2": { "name": "keyword.control.import.preprocessor.sourcelocation.swift" }, + "4": { "name": "punctuation.definition.parameters.begin.swift" }, + "5": { + "patterns": [ + { + "begin": "(file)\\s*(:)\\s*(?=\")", + "end": "(?!\\G)", + "beginCaptures": { + "1": { "name": "support.variable.parameter.swift" }, + "2": { "name": "punctuation.separator.key-value.swift" } + }, + "patterns": [{ "include": "#literals" }] + }, + + { + "match": "(line)\\s*(:)\\s*([0-9]+)", + "captures": { + "1": { "name": "support.variable.parameter.swift" }, + "2": { "name": "punctuation.separator.key-value.swift" }, + "3": { "name": "constant.numeric.integer.swift" } + } + }, + + { + "name": "punctuation.separator.parameters.swift", + "match": "," + }, + + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "\\S+" + } + ] + }, + "6": { "name": "punctuation.definition.parameters.begin.swift" }, + "7": { + "patterns": [ + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "\\S+" + } + ] + } + } + } + ] + }, + + "conditionals": { + "patterns": [ + { + "begin": "(?&|\\^~.])(->)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { "name": "keyword.operator.type.function.swift" } + } + }, + + { + "comment": "Swift 3: A & B", + "match": "(?&|\\^~.])(&)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { "name": "keyword.operator.type.composition.swift" } + } + }, + + { + "name": "keyword.operator.type.optional.swift", + "match": "[?!]" + }, + + { + "name": "keyword.operator.function.variadic-parameter.swift", + "match": "\\.\\.\\." + }, + + { + "comment": "Swift 2: protocol", + "name": "keyword.other.type.composition.swift", + "match": "\\bprotocol\\b" + }, + + { + "name": "keyword.other.type.metatype.swift", + "match": "(?<=\\.)(?:Protocol|Type)\\b" + }, + + { "include": "#declarations-available-types-tuple-type" }, + { "include": "#declarations-available-types-collection-type" }, + { "include": "#declarations-generic-argument-clause" } + ] + }, + + "declarations-available-types-collection-type": { + "comment": "array and dictionary types [Value] and [Key: Value]", + "begin": "\\[", + "end": "\\]|(?=[>){}])", + "beginCaptures": { + "0": { "name": "punctuation.section.collection-type.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.section.collection-type.end.swift" } + }, + "patterns": [ + { "include": "#declarations-available-types" }, + { "include": "#literals-numeric" }, + + { + "name": "support.variable.inferred.swift", + "match": "\\b_\\b" + }, + + { + "name": "keyword.other.inline-array.swift", + "match": "(?<=\\s)\\bof\\b(?=\\s+[\\p{L}_\\d\\p{N}\\p{M}\\[(])" + }, + + { + "begin": ":", + "end": "(?=\\]|[>){}])", + "beginCaptures": { + "0": { "name": "punctuation.separator.key-value.swift" } + }, + "patterns": [ + { + "name": "invalid.illegal.extra-colon-in-dictionary-type.swift", + "match": ":" + }, + { "include": "#declarations-available-types" } + ] + } + ] + }, + + "declarations-available-types-tuple-type": { + "begin": "\\(", + "end": "\\)|(?=[>\\]{}])", + "beginCaptures": { + "0": { "name": "punctuation.section.tuple-type.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.section.tuple-type.end.swift" } + }, + "patterns": [{ "include": "#declarations-available-types" }] + }, + + "declarations-extension": { + "name": "meta.definition.type.$1.swift", + "begin": "\\b(extension)\\s+", + "end": "(?<=\\})", + "beginCaptures": { + "1": { "name": "storage.type.$1.swift" } + }, + "patterns": [ + { + "name": "entity.name.type.swift", + "begin": "\\G(?!\\s*[:{\\n])", + "end": "(?=\\s*[:{\\n])|(?!\\G)(?=\\s*where\\b)", + "patterns": [{ "include": "#declarations-available-types" }] + }, + { "include": "#comments" }, + { + "comment": "SE-0143: Conditional Conformances", + "include": "#declarations-generic-where-clause" + }, + { "include": "#declarations-inheritance-clause" }, + + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.type.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.type.end.swift" } + }, + "patterns": [{ "include": "$self" }] + } + ] + }, + + "declarations-function": { + "name": "meta.definition.function.swift", + "begin": "(?x)\n\\b\n(func)\n\\s+\n(\n (?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\n | (?:\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*\n )\n | ( \\. ( \\g | \\g | \\. )+ ) # Dot operators\n )\n)\n\\s*\n(?=\\(|<)", + "end": "(?<=\\})|$(?# functions in protocol declarations or generated interfaces have no body)", + "beginCaptures": { + "1": { "name": "storage.type.function.swift" }, + "2": { "name": "entity.name.function.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-generic-parameter-clause" }, + { "include": "#declarations-parameter-clause" }, + { "include": "#declarations-function-result" }, + { "include": "#async-throws" }, + { + "comment": "Swift 3: generic constraints after the parameters and return type", + "include": "#declarations-generic-where-clause" + }, + + { + "name": "meta.definition.function.body.swift", + "begin": "(\\{)", + "end": "(\\})", + "beginCaptures": { + "1": { "name": "punctuation.section.function.begin.swift" } + }, + "endCaptures": { + "1": { "name": "punctuation.section.function.end.swift" } + }, + "patterns": [{ "include": "$self" }] + } + ] + }, + + "declarations-function-initializer": { + "name": "meta.definition.function.initializer.swift", + "begin": "(?&|\\^~.])(->)(?![/=\\-+!*%<>&|\\^~.])\\s*", + "end": "(?!\\G)(?=\\{|\\bwhere\\b|;|=)|$", + "beginCaptures": { + "1": { "name": "keyword.operator.function-result.swift" } + }, + "patterns": [ + { + "name": "storage.modifier.swift", + "match": "\\bsending\\b" + }, + { "include": "#declarations-available-types" } + ] + }, + + "declarations-function-subscript": { + "name": "meta.definition.function.subscript.swift", + "begin": "(?|(?=[)\\]{}])", + "beginCaptures": { + "0": { "name": "punctuation.separator.generic-argument-clause.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.separator.generic-argument-clause.end.swift" } + }, + "patterns": [ + { "include": "#literals-numeric" }, + { "include": "#declarations-available-types" } + ] + }, + + "declarations-generic-parameter-clause": { + "name": "meta.generic-parameter-clause.swift", + "begin": "<", + "end": ">|(?=[^\\w\\d:<>\\s,=&`])(?# characters besides these are never valid in a generic param list -- even if it's not really a valid clause, we should stop trying to parse it if we see one of them.)", + "beginCaptures": { + "0": { "name": "punctuation.separator.generic-parameter-clause.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.separator.generic-parameter-clause.end.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { + "comment": "Swift 2: constraints inside the generic param list", + "include": "#declarations-generic-where-clause" + }, + + { + "name": "keyword.other.declaration-specifier.swift", + "match": "\\blet\\b" + }, + + { + "name": "keyword.control.loop.swift", + "match": "\\beach\\b" + }, + + { + "match": "\\b((?!\\d)\\w[\\w\\d]*)\\b", + "captures": { + "1": { "name": "variable.language.generic-parameter.swift" } + } + }, + + { + "name": "punctuation.separator.generic-parameters.swift", + "match": "," + }, + + { + "name": "meta.generic-parameter-constraint.swift", + "begin": "(:)\\s*", + "end": "(?=[,>]|(?!\\G)\\bwhere\\b)", + "beginCaptures": { + "1": { "name": "punctuation.separator.generic-parameter-constraint.swift" } + }, + "patterns": [ + { + "name": "entity.other.inherited-class.swift", + "begin": "\\G", + "end": "(?=[,>]|(?!\\G)\\bwhere\\b)", + "patterns": [ + { "include": "#declarations-type-identifier" }, + { "include": "#declarations-type-operators" } + ] + } + ] + } + ] + }, + + "declarations-generic-where-clause": { + "name": "meta.generic-where-clause.swift", + "begin": "\\b(where)\\b\\s*", + "end": "(?!\\G)$|(?=[>{};\\n]|//|/\\*)", + "beginCaptures": { + "1": { "name": "keyword.other.generic-constraint-introducer.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-generic-where-clause-requirement-list" } + ] + }, + + "declarations-generic-where-clause-requirement-list": { + "begin": "\\G|,\\s*", + "end": "(?=[,>{};\\n]|//|/\\*)", + "patterns": [ + { "include": "#comments" }, + { "include": "#constraint" }, + { "include": "#declarations-available-types" }, + + { + "name": "meta.generic-where-clause.same-type-requirement.swift", + "begin": "(?&|\\^~.])(==)(?![/=\\-+!*%<>&|\\^~.])", + "end": "(?=\\s*[,>{};\\n]|//|/\\*)", + "beginCaptures": { + "1": { "name": "keyword.operator.generic-constraint.same-type.swift" } + }, + "patterns": [{ "include": "#declarations-available-types" }] + }, + + { + "name": "meta.generic-where-clause.conformance-requirement.swift", + "begin": "(?&|\\^~.])(:)(?![/=\\-+!*%<>&|\\^~.])", + "end": "(?=\\s*[,>{};\\n]|//|/\\*)", + "beginCaptures": { + "1": { "name": "keyword.operator.generic-constraint.conforms-to.swift" } + }, + "patterns": [ + { + "contentName": "entity.other.inherited-class.swift", + "begin": "\\G\\s*", + "end": "(?=\\s*[,>{};\\n]|//|/\\*)", + "patterns": [{ "include": "#declarations-available-types" }] + } + ] + } + ] + }, + + "declarations-import": { + "name": "meta.import.swift", + "begin": "(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)", + "captures": { + "1": { "name": "punctuation.definition.identifier.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" } + } + }, + + { + "name": "entity.name.type.swift", + "match": "(?x)\n(?<=\\G|\\.)\n\\$[0-9]+" + }, + + { + "name": "entity.name.type.swift", + "match": "(?x)\n(?<=\\G|\\.)\n(?:\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*\n )\n | ( \\. ( \\g | \\g | \\. )+ ) # Dot operators\n)\n(?=\\.|;|$|//|/\\*|\\s)", + "captures": { + "1": { + "patterns": [ + { + "name": "invalid.illegal.dot-not-allowed-here.swift", + "match": "\\." + } + ] + } + } + }, + + { + "name": "punctuation.separator.import.swift", + "match": "\\." + }, + + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "begin": "(?!\\s*(;|$|//|/\\*))", + "end": "(?=\\s*(;|$|//|/\\*))" + } + ] + } + ] + }, + + "declarations-inheritance-clause": { + "name": "meta.inheritance-clause.swift", + "begin": "(:)(?=\\s*\\{)|(:)\\s*", + "end": "(?!\\G)$|(?=[={}]|(?!\\G)\\bwhere\\b)", + "beginCaptures": { + "1": { "name": "invalid.illegal.empty-inheritance-clause.swift" }, + "2": { "name": "punctuation.separator.inheritance-clause.swift" } + }, + "patterns": [ + { + "begin": "\\bclass\\b", + "end": "(?=[={}]|(?!\\G)\\bwhere\\b)", + "beginCaptures": { + "0": { "name": "storage.type.class.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-inheritance-clause-more-types" } + ] + }, + + { + "begin": "\\G", + "end": "(?!\\G)$|(?=[={}]|(?!\\G)\\bwhere\\b)", + "patterns": [ + { "include": "#attributes" }, + { "include": "#comments" }, + { "include": "#declarations-inheritance-clause-inherited-type" }, + { "include": "#declarations-inheritance-clause-more-types" }, + { "include": "#declarations-type-operators" } + ] + } + ] + }, + + "declarations-inheritance-clause-inherited-type": { + "name": "entity.other.inherited-class.swift", + "begin": "(?=[`\\p{L}_])", + "end": "(?!\\G)", + "patterns": [{ "include": "#declarations-type-identifier" }] + }, + + "declarations-inheritance-clause-more-types": { + "name": "meta.inheritance-list.more-types", + "begin": ",\\s*", + "end": "(?!\\G)(?!//|/\\*)|(?=[,={}]|(?!\\G)\\bwhere\\b)", + "patterns": [ + { "include": "#attributes" }, + { "include": "#comments" }, + { "include": "#declarations-inheritance-clause-inherited-type" }, + { "include": "#declarations-inheritance-clause-more-types" }, + { "include": "#declarations-type-operators" } + ] + }, + + "declarations-macro": { + "name": "meta.definition.macro.swift", + "begin": "(?x)\n\\b\n(macro)\n\\s+\n((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\n\\s*\n(?=\\(|<|=)", + "end": "$|(?=;|//|/\\*|\\}|=)", + "beginCaptures": { + "1": { "name": "storage.type.function.swift" }, + "2": { "name": "entity.name.function.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-generic-parameter-clause" }, + { "include": "#declarations-parameter-clause" }, + { "include": "#declarations-function-result" }, + { "include": "#async-throws" }, + { + "comment": "Swift 3: generic constraints after the parameters and return type", + "include": "#declarations-generic-where-clause" + } + ] + }, + + "declarations-operator": { + "name": "meta.definition.operator.swift", + "begin": "(?x)\n(?:\n \\b(prefix|infix|postfix)\n \\s+\n)?\n\\b\n(operator)\n\\s+\n(\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | \\. # Invalid dot\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*+\n )\n | ( \\. ( \\g | \\g | \\. )++ ) # Dot operators\n)\n\\s*", + "end": "(;)|$\\n?|(?=//|/\\*)", + "beginCaptures": { + "1": { "name": "storage.modifier.swift" }, + "2": { "name": "storage.type.function.operator.swift" }, + "3": { "name": "entity.name.function.operator.swift" }, + "4": { + "comment": "workaround for https://github.com/microsoft/vscode-textmate/issues/140#issuecomment-1793610346", + "name": "entity.name.function.operator.swift", + "patterns": [ + { + "name": "invalid.illegal.dot-not-allowed-here.swift", + "match": "\\." + } + ] + } + }, + "endCaptures": { + "1": { "name": "punctuation.terminator.statement.swift" } + }, + "patterns": [ + { "include": "#declarations-operator-swift2" }, + { "include": "#declarations-operator-swift3" }, + + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "((?!$|;|//|/\\*)\\S)+" + } + ] + }, + + "declarations-operator-swift2": { + "begin": "\\G(\\{)", + "end": "(\\})", + "beginCaptures": { + "1": { "name": "punctuation.definition.operator.begin.swift" } + }, + "endCaptures": { + "1": { "name": "punctuation.definition.operator.end.swift" } + }, + "patterns": [ + { "include": "#comments" }, + + { + "match": "\\b(associativity)\\s+(left|right)\\b", + "captures": { + "1": { "name": "storage.modifier.swift" }, + "2": { "name": "keyword.other.operator.associativity.swift" } + } + }, + + { + "match": "\\b(precedence)\\s+([0-9]+)\\b", + "captures": { + "1": { "name": "storage.modifier.swift" }, + "2": { "name": "constant.numeric.integer.swift" } + } + }, + + { + "match": "\\b(assignment)\\b", + "captures": { + "1": { "name": "storage.modifier.swift" } + } + } + ] + }, + + "declarations-operator-swift3": { + "match": "\\G(:)\\s*((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "captures": { + "2": { + "name": "entity.other.inherited-class.swift", + "patterns": [{ "include": "#declarations-types-precedencegroup" }] + }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + } + }, + + "declarations-parameter-clause": { + "name": "meta.parameter-clause.swift", + "begin": "(\\()", + "end": "(\\))(?:\\s*(async)\\b)?", + "beginCaptures": { + "1": { "name": "punctuation.definition.parameters.begin.swift" } + }, + "endCaptures": { + "1": { "name": "punctuation.definition.parameters.end.swift" }, + "2": { "name": "storage.modifier.async.swift" } + }, + "patterns": [{ "include": "#declarations-parameter-list" }] + }, + + "declarations-parameter-list": { + "patterns": [ + { + "comment": "External parameter labels are considered part of the function name", + "match": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))(?=\\s*:)", + "captures": { + "1": { "name": "entity.name.function.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "variable.parameter.function.swift" }, + "5": { "name": "punctuation.definition.identifier.swift" }, + "6": { "name": "punctuation.definition.identifier.swift" } + } + }, + + { + "comment": "If no external label is given, the name is both the external label and the internal variable name", + "match": "(((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)))(?=\\s*:)", + "captures": { + "1": { "name": "variable.parameter.function.swift" }, + "2": { "name": "entity.name.function.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + } + }, + + { + "begin": ":\\s*(?!\\s)", + "end": "(?=[,)])", + "patterns": [ + { + "name": "storage.modifier.swift", + "match": "\\bsending\\b" + }, + { "include": "#declarations-available-types" }, + + { + "name": "invalid.illegal.extra-colon-in-parameter-list.swift", + "match": ":" + }, + + { + "comment": "a parameter's default value", + "begin": "=", + "end": "(?=[,)])", + "beginCaptures": { + "0": { "name": "keyword.operator.assignment.swift" } + }, + "patterns": [{ "include": "#expressions" }] + } + ] + } + ] + }, + + "declarations-precedencegroup": { + "name": "meta.definition.precedencegroup.swift", + "begin": "\\b(precedencegroup)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(?=\\{)", + "end": "(?!\\G)", + "beginCaptures": { + "1": { "name": "storage.type.precedencegroup.swift" }, + "2": { "name": "entity.name.type.precedencegroup.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.precedencegroup.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.precedencegroup.end.swift" } + }, + "patterns": [ + { "include": "#comments" }, + + { + "match": "\\b(higherThan|lowerThan)\\s*:\\s*((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "captures": { + "1": { "name": "storage.modifier.swift" }, + "2": { + "name": "entity.other.inherited-class.swift", + "patterns": [{ "include": "#declarations-types-precedencegroup" }] + }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + } + }, + + { + "match": "\\b(associativity)\\b(?:\\s*:\\s*(right|left|none)\\b)?", + "captures": { + "1": { "name": "storage.modifier.swift" }, + "2": { "name": "keyword.other.operator.associativity.swift" } + } + }, + + { + "match": "\\b(assignment)\\b(?:\\s*:\\s*(true|false)\\b)?", + "captures": { + "1": { "name": "storage.modifier.swift" }, + "2": { "name": "constant.language.boolean.swift" } + } + } + ] + } + ] + }, + + "declarations-protocol": { + "name": "meta.definition.type.protocol.swift", + "begin": "\\b(protocol)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { "name": "storage.type.$1.swift" }, + "2": { "name": "entity.name.type.$1.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-inheritance-clause" }, + { + "comment": "SE-0142: Permit where clauses to constrain associated types", + "include": "#declarations-generic-where-clause" + }, + + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.type.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.type.end.swift" } + }, + "patterns": [ + { "include": "#declarations-protocol-protocol-method" }, + { "include": "#declarations-protocol-protocol-initializer" }, + { "include": "#declarations-protocol-associated-type" }, + { "include": "$self" } + ] + } + ] + }, + + "declarations-protocol-associated-type": { + "name": "meta.definition.associatedtype.swift", + "begin": "\\b(associatedtype)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?!\\G)$|(?=[;}]|$)", + "beginCaptures": { + "1": { "name": "keyword.other.declaration-specifier.swift" }, + "2": { "name": "variable.language.associatedtype.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#declarations-inheritance-clause" }, + { + "comment": "SE-0142: Permit where clauses to constrain associated types", + "include": "#declarations-generic-where-clause" + }, + { "include": "#declarations-typealias-assignment" } + ] + }, + + "declarations-protocol-protocol-initializer": { + "name": "meta.definition.function.initializer.swift", + "begin": "(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\n | (?:\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*\n )\n | ( \\. ( \\g | \\g | \\. )+ ) # Dot operators\n )\n )\n\\s*\n(?=\\(|<)", + "end": "$|(?=;|//|/\\*|\\})", + "beginCaptures": { + "1": { "name": "storage.type.function.swift" }, + "2": { "name": "entity.name.function.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-generic-parameter-clause" }, + { "include": "#declarations-parameter-clause" }, + { "include": "#declarations-function-result" }, + { "include": "#async-throws" }, + { + "comment": "Swift 3: generic constraints after the parameters and return type", + "include": "#declarations-generic-where-clause" + }, + + { + "name": "invalid.illegal.function-body-not-allowed-in-protocol.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.section.function.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.section.function.end.swift" } + }, + "patterns": [{ "include": "$self" }] + } + ] + }, + + "declarations-type": { + "patterns": [ + { + "name": "meta.definition.type.$1.swift", + "begin": "\\b(class(?!\\s+(?:func|var|let)\\b)|struct|actor)\\b\\s*((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { "name": "storage.type.$1.swift" }, + "2": { "name": "entity.name.type.$1.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-generic-parameter-clause" }, + { + "comment": "Swift 3: generic constraints after the generic param list", + "include": "#declarations-generic-where-clause" + }, + { "include": "#declarations-inheritance-clause" }, + + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.type.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.type.end.swift" } + }, + "patterns": [{ "include": "$self" }] + } + ] + }, + { "include": "#declarations-type-enum" } + ] + }, + + "declarations-type-enum": { + "name": "meta.definition.type.$1.swift", + "begin": "\\b(enum)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { "name": "storage.type.$1.swift" }, + "2": { "name": "entity.name.type.$1.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-generic-parameter-clause" }, + { + "comment": "Swift 3: generic constraints after the generic param list", + "include": "#declarations-generic-where-clause" + }, + { "include": "#declarations-inheritance-clause" }, + + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.type.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.type.end.swift" } + }, + "patterns": [ + { "include": "#declarations-type-enum-enum-case-clause" }, + { "include": "$self" } + ] + } + ] + }, + + "declarations-type-enum-associated-values": { + "begin": "\\G\\(", + "end": "\\)", + "beginCaptures": { + "0": { "name": "punctuation.definition.parameters.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.parameters.end.swift" } + }, + "patterns": [ + { "include": "#comments" }, + + { + "begin": "(?x)\n(?:(_)|((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*\\k))\n\\s+\n(((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*\\k))\n\\s*(:)", + "end": "(?=[,)\\]])", + "beginCaptures": { + "1": { "name": "entity.name.function.swift" }, + "2": { "name": "invalid.illegal.distinct-labels-not-allowed.swift" }, + "5": { "name": "variable.parameter.function.swift" }, + "7": { "name": "punctuation.separator.argument-label.swift" } + }, + "patterns": [{ "include": "#declarations-available-types" }] + }, + + { + "begin": "(((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*\\k))\\s*(:)", + "end": "(?=[,)\\]])", + "beginCaptures": { + "1": { "name": "entity.name.function.swift" }, + "2": { "name": "variable.parameter.function.swift" }, + "4": { "name": "punctuation.separator.argument-label.swift" } + }, + "patterns": [{ "include": "#declarations-available-types" }] + }, + + { + "comment": "an element without a label (i.e. anything else)", + "begin": "(?![,)\\]])(?=\\S)", + "end": "(?=[,)\\]])", + "patterns": [ + { "include": "#declarations-available-types" }, + + { + "name": "invalid.illegal.extra-colon-in-parameter-list.swift", + "match": ":" + } + ] + } + ] + }, + + "declarations-type-enum-enum-case": { + "begin": "(?x)((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?<=\\))|(?![=(])", + "beginCaptures": { + "1": { "name": "variable.other.enummember.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-type-enum-associated-values" }, + { "include": "#declarations-type-enum-raw-value-assignment" } + ] + }, + + "declarations-type-enum-enum-case-clause": { + "begin": "\\b(case)\\b\\s*", + "end": "(?=[;}])|(?!\\G)(?!//|/\\*)(?=[^\\s,])", + "beginCaptures": { + "1": { "name": "storage.type.enum.case.swift" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-type-enum-enum-case" }, + { "include": "#declarations-type-enum-more-cases" } + ] + }, + + "declarations-type-enum-more-cases": { + "name": "meta.enum-case.more-cases", + "begin": ",\\s*", + "end": "(?!\\G)(?!//|/\\*)(?=[;}]|[^\\s,])", + "patterns": [ + { "include": "#comments" }, + { "include": "#declarations-type-enum-enum-case" }, + { "include": "#declarations-type-enum-more-cases" } + ] + }, + + "declarations-type-enum-raw-value-assignment": { + "begin": "(=)\\s*", + "end": "(?!\\G)", + "beginCaptures": { + "1": { "name": "keyword.operator.assignment.swift" } + }, + "patterns": [{ "include": "#comments" }, { "include": "#literals" }] + }, + + "declarations-type-identifier": { + "begin": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?!<)", + "beginCaptures": { + "1": { + "name": "meta.type-name.swift", + "patterns": [{ "include": "#builtin-types" }] + }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { + "begin": "(?=<)", + "end": "(?!\\G)", + "patterns": [{ "include": "#declarations-generic-argument-clause" }] + } + ] + }, + + "declarations-type-operators": { + "patterns": [ + { + "comment": "Swift 3: A & B", + "match": "(?&|\\^~.])(&)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { "name": "keyword.operator.type.composition.swift" } + } + }, + + { + "comment": "SE-0390: Noncopyable structs and enums", + "match": "(?&|\\^~.])(~)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { "name": "keyword.operator.type.requirement-suppression.swift" } + } + } + ] + }, + + "declarations-typealias": { + "name": "meta.definition.typealias.swift", + "begin": "\\b(typealias)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?!\\G)$|(?=;|//|/\\*|$)", + "beginCaptures": { + "1": { "name": "keyword.other.declaration-specifier.swift" }, + "2": { "name": "entity.name.type.typealias.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.identifier.swift" } + }, + "patterns": [ + { + "begin": "\\G(?=<)", + "end": "(?!\\G)", + "patterns": [{ "include": "#declarations-generic-parameter-clause" }] + }, + { "include": "#declarations-typealias-assignment" } + ] + }, + + "declarations-typealias-assignment": { + "begin": "(=)\\s*", + "end": "(?!\\G)$|(?=;|//|/\\*|$)", + "beginCaptures": { + "1": { "name": "keyword.operator.assignment.swift" } + }, + "patterns": [{ "include": "#declarations-available-types" }] + }, + + "declarations-typed-variable-declaration": { + "begin": "(?x)\n\\b(?:(async)\\s+)?(let|var)\\b\\s+\n(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\\s*\n:", + "end": "(?=$|[={])", + "beginCaptures": { + "1": { "name": "storage.modifier.async.swift" }, + "2": { "name": "keyword.other.declaration-specifier.swift" } + }, + "patterns": [{ "include": "#declarations-available-types" }] + }, + + "declarations-types-precedencegroup": { + "patterns": [ + { + "comment": "Precedence groups in the standard library", + "name": "support.type.swift", + "match": "\\b(?:BitwiseShift|Assignment|RangeFormation|Casting|Addition|NilCoalescing|Comparison|LogicalConjunction|LogicalDisjunction|Default|Ternary|Multiplication|FunctionArrow)Precedence\\b" + } + ] + }, + + "expressions": { + "comment": "trailing closures need to be parsed before other member references", + "patterns": [ + { "include": "#expressions-without-trailing-closures-or-member-references" }, + { "include": "#expressions-trailing-closure" }, + { "include": "#member-reference" } + ] + }, + + "expressions-trailing-closure": { + "patterns": [ + { + "comment": "foo { body } -- a call with a trailing closure and no argument clause", + "name": "meta.function-call.trailing-closure-only.swift", + "match": "(#?(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))(?=\\s*\\{)", + "captures": { + "1": { "name": "support.function.any-method.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" } + } + }, + + { + "comment": "foo: { body } -- labeled-trailing-closure (SE-0279)", + "match": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(:)(?=\\s*\\{)", + "captures": { + "1": { "name": "support.function.any-method.trailing-closure-label.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.separator.argument-label.swift" } + } + } + ] + }, + + "expressions-without-trailing-closures": { + "patterns": [ + { "include": "#expressions-without-trailing-closures-or-member-references" }, + { "include": "#member-references" } + ] + }, + + "expressions-without-trailing-closures-or-member-references": { + "patterns": [ + { "include": "#comments" }, + { "include": "#code-block" }, + { "include": "#attributes" }, + { + "include": "#expressions-without-trailing-closures-or-member-references-closure-parameter" + }, + { "include": "#literals" }, + { "include": "#operators" }, + { "include": "#builtin-types" }, + { "include": "#builtin-functions" }, + { "include": "#builtin-global-functions" }, + { "include": "#builtin-properties" }, + { + "include": "#expressions-without-trailing-closures-or-member-references-compound-name" + }, + { "include": "#conditionals" }, + { "include": "#keywords" }, + { + "include": "#expressions-without-trailing-closures-or-member-references-availability-condition" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-function-or-macro-call-expression" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-macro-expansion" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-subscript-expression" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-parenthesized-expression" + }, + + { + "name": "support.variable.discard-value.swift", + "match": "\\b_\\b" + } + ] + }, + + "expressions-without-trailing-closures-or-member-references-availability-condition": { + "begin": "\\B(#(?:un)?available)(\\()", + "end": "\\)", + "beginCaptures": { + "1": { "name": "support.function.availability-condition.swift" }, + "2": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [ + { + "match": "\\s*\\b((?:iOS|macOS|OSX|watchOS|tvOS|visionOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))", + "captures": { + "1": { "name": "keyword.other.platform.os.swift" }, + "2": { "name": "constant.numeric.swift" } + } + }, + + { + "match": "(\\*)\\s*(.*?)(?=[,)])", + "captures": { + "1": { "name": "keyword.other.platform.all.swift" }, + "2": { "name": "invalid.illegal.character-not-allowed-here.swift" } + } + }, + + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "[^\\s,)]+" + } + ] + }, + + "expressions-without-trailing-closures-or-member-references-closure-parameter": { + "name": "variable.language.closure-parameter.swift", + "match": "\\$[0-9]+" + }, + + "expressions-without-trailing-closures-or-member-references-compound-name": { + "comment": "a reference to a function with disambiguating argument labels, such as foo(_:), foo(bar:), etc.", + "match": "(?x)\n((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)) # function name\n\\(\n (\n (\n ((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)) # argument label\n : # colon\n )+\n )\n\\)", + "captures": { + "1": { "name": "entity.name.function.compound-name.swift" }, + "2": { "name": "punctuation.definition.entity.swift" }, + "3": { "name": "punctuation.definition.entity.swift" }, + "4": { + "patterns": [ + { + "name": "entity.name.function.compound-name.swift", + "match": "(?`?)(?!_:)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k):", + "captures": { + "1": { "name": "punctuation.definition.entity.swift" }, + "2": { "name": "punctuation.definition.entity.swift" } + } + } + ] + } + } + }, + + "expressions-without-trailing-closures-or-member-references-expression-element-list": { + "patterns": [ + { "include": "#comments" }, + + { + "comment": "an element with a label", + "begin": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(:)", + "end": "(?=[,)\\]])", + "beginCaptures": { + "1": { "name": "support.function.any-method.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.separator.argument-label.swift" } + }, + "patterns": [{ "include": "#expressions" }] + }, + + { + "comment": "an element without a label (i.e. anything else)", + "begin": "(?![,)\\]])(?=\\S)", + "end": "(?=[,)\\]])", + "patterns": [{ "include": "#expressions" }] + } + ] + }, + + "expressions-without-trailing-closures-or-member-references-function-or-macro-call-expression": { + "patterns": [ + { + "comment": "foo(args) -- a call whose callee is a highlightable name", + "name": "meta.function-call.swift", + "begin": "(#?(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(\\()", + "end": "\\)", + "beginCaptures": { + "1": { "name": "support.function.any-method.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" }, + "4": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + }, + + { + "comment": "[Int](args) -- a call whose callee is a more complicated expression", + "name": "meta.function-call.swift", + "begin": "(?<=[`\\])}>\\p{L}_\\p{N}\\p{M}])\\s*(\\()", + "end": "\\)", + "beginCaptures": { + "1": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + } + ] + }, + + "expressions-without-trailing-closures-or-member-references-macro-expansion": { + "name": "support.function.any-method.swift", + "match": "(#(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))" + }, + + "expressions-without-trailing-closures-or-member-references-parenthesized-expression": { + "comment": "correctly matching closure expressions is too hard (depends on trailing \"in\") so we just tack on some basics to the end of parenthesized-expression", + "begin": "\\(", + "end": "(\\))\\s*((?:\\b(?:async|throws|rethrows)\\s)*)", + "beginCaptures": { + "0": { "name": "punctuation.section.tuple.begin.swift" } + }, + "endCaptures": { + "1": { "name": "punctuation.section.tuple.end.swift" }, + "2": { + "patterns": [ + { + "name": "invalid.illegal.rethrows-only-allowed-on-function-declarations.swift", + "match": "\\brethrows\\b" + }, + { "include": "#async-throws" } + ] + } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + }, + + "expressions-without-trailing-closures-or-member-references-subscript-expression": { + "name": "meta.subscript-expression.swift", + "begin": "(?<=[`\\p{L}_\\p{N}\\p{M}])\\s*(\\[)", + "end": "\\]", + "beginCaptures": { + "1": { "name": "punctuation.definition.arguments.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.arguments.end.swift" } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + }, + + "keywords": { + "patterns": [ + { + "name": "keyword.control.branch.swift", + "match": "(?\n (?> # no backtracking, avoids issues with negative lookbehind at end\n (?:\n \\\\Q\n (?:(?!\\\\E)(?!/).)*+\n # A quoted sequence may not have a closing E, in which case it extends to the end of the regex\n (?:\\\\E | (?=/))\n | \\\\.\n | \\(\\?\\#[^)]*\\)\n | \\(\\?\n # InterpolatedCallout\n (?>\n {[^{].*?}\n | {{[^{].*?}}\n | {{{[^{].*?}}}\n | {{{{[^{].*?}}}}\n | {{{{{[^{].*?}}}}}\n | {{{{{{.+?}}}}}}\n )\n (?:\\[(?!\\d)\\w+\\])?\n [X<>]?\n \\)\n # Allow nested character classes to a limited depth\n | \\[(?:\n \\\\. |\n [^\\[\\]\\\\] |\n \\[(?:\n \\\\. |\n [^\\[\\]\\\\] |\n \\[(?:\n \\\\. |\n [^\\[\\]\\\\] |\n \\[(?:\n \\\\. |\n [^\\[\\]\\\\]\n )+\\]\n )+\\]\n )+\\]\n )+\\]\n | \\(\\g?+\\)\n | (?:(?!/)[^()\\[\\\\])+ # any character (until end)\n )+\n )\n )?+\n (?\n (?> # no backtracking, avoids issues with negative lookbehind at end\n (?:\n \\\\Q\n (?:(?!\\\\E)(?!/\\2).)*+\n # A quoted sequence may not have a closing E, in which case it extends to the end of the regex\n (?:\\\\E | (?=/\\2))\n | \\\\.\n | \\(\\?\\#[^)]*\\)\n | \\(\\?\n # InterpolatedCallout\n (?>\n {[^{].*?}\n | {{[^{].*?}}\n | {{{[^{].*?}}}\n | {{{{[^{].*?}}}}\n | {{{{{[^{].*?}}}}}\n | {{{{{{.+?}}}}}}\n )\n (?:\\[(?!\\d)\\w+\\])?\n [X<>]?\n \\)\n # Allow nested character classes to a limited depth\n | \\[(?:\n \\\\. |\n [^\\[\\]\\\\] |\n \\[(?:\n \\\\. |\n [^\\[\\]\\\\] |\n \\[(?:\n \\\\. |\n [^\\[\\]\\\\] |\n \\[(?:\n \\\\. |\n [^\\[\\]\\\\]\n )+\\]\n )+\\]\n )+\\]\n )+\\]\n | \\(\\g?+\\)\n | (?:(?!/\\2)[^()\\[\\\\])+ # any character (until end)\n )+\n )\n)?+\n(/\\2) # (4)\n| \\#+/.+(\\n)", + "captures": { + "0": { + "patterns": [{ "include": "#literals-regular-expression-literal-regex-guts" }] + }, + "1": { "name": "punctuation.definition.string.begin.regexp.swift" }, + "4": { "name": "punctuation.definition.string.end.regexp.swift" }, + "5": { "name": "invalid.illegal.returns-not-allowed.regexp" } + } + } + ] + }, + + "literals-regular-expression-literal-backreference-or-subpattern": { + "comment": "These patterns are separated to work around issues like https://github.com/microsoft/vscode-textmate/issues/164", + "patterns": [ + { + "comment": "'\\g{' NamedOrNumberRef '}'", + "match": "(?x)(\\\\g\\{) (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (\\})", + "captures": { + "1": { "name": "constant.character.escape.backslash.regexp" }, + "2": { "name": "variable.other.group-name.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "constant.numeric.integer.decimal.regexp" }, + "6": { "name": "keyword.operator.recursion-level.regexp" }, + "7": { "name": "constant.numeric.integer.decimal.regexp" }, + "8": { "name": "constant.character.escape.backslash.regexp" } + } + }, + + { + "comment": "'\\g' NumberRef", + "match": "(?x)(\\\\g) ([+-]?\\d+) (?:([+-])(\\d+))?", + "captures": { + "1": { "name": "constant.character.escape.backslash.regexp" }, + "2": { "name": "constant.numeric.integer.decimal.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" } + } + }, + + { + "comment": "'\\k<' NamedOrNumberRef '>'\n '\\g<' NamedOrNumberRef '>'", + "match": "(?x)(\\\\[gk]<) (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (>)", + "captures": { + "1": { "name": "constant.character.escape.backslash.regexp" }, + "2": { "name": "variable.other.group-name.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "constant.numeric.integer.decimal.regexp" }, + "6": { "name": "keyword.operator.recursion-level.regexp" }, + "7": { "name": "constant.numeric.integer.decimal.regexp" }, + "8": { "name": "constant.character.escape.backslash.regexp" } + } + }, + + { + "comment": "\"\\k'\" NamedOrNumberRef \"'\"\n \"\\g'\" NamedOrNumberRef \"'\"", + "match": "(?x)(\\\\[gk]') (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (')", + "captures": { + "1": { "name": "constant.character.escape.backslash.regexp" }, + "2": { "name": "variable.other.group-name.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "constant.numeric.integer.decimal.regexp" }, + "6": { "name": "keyword.operator.recursion-level.regexp" }, + "7": { "name": "constant.numeric.integer.decimal.regexp" }, + "8": { "name": "constant.character.escape.backslash.regexp" } + } + }, + + { + "comment": "'\\k{' NamedRef '}'", + "match": "(?x)(\\\\k\\{) ((?!\\d)\\w+) (?:([+-])(\\d+))? (\\})", + "captures": { + "1": { "name": "constant.character.escape.backslash.regexp" }, + "2": { "name": "variable.other.group-name.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "constant.character.escape.backslash.regexp" } + } + }, + + { + "name": "keyword.other.back-reference.regexp", + "match": "\\\\[1-9][0-9]+" + }, + + { + "comment": "'(?P=' NamedRef ')'", + "match": "(?x)(\\(\\?(?:P[=>]|&)) ((?!\\d)\\w+) (?:([+-])(\\d+))? (\\))", + "captures": { + "1": { "name": "keyword.other.back-reference.regexp" }, + "2": { "name": "variable.other.group-name.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "keyword.other.back-reference.regexp" } + } + }, + + { + "name": "keyword.other.back-reference.regexp", + "match": "\\(\\?R\\)" + }, + + { + "comment": "'(?' NumberRef ')'", + "match": "(?x)(\\(\\?) ([+-]?\\d+) (?:([+-])(\\d+))? (\\))", + "captures": { + "1": { "name": "keyword.other.back-reference.regexp" }, + "2": { "name": "constant.numeric.integer.decimal.regexp" }, + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "keyword.other.back-reference.regexp" } + } + } + ] + }, + + "literals-regular-expression-literal-backtracking-directive-or-global-matching-option": { + "match": "(?x)\n(\\(\\*)\n(?:\n (ACCEPT|FAIL|F|MARK(?=:)|(?=:)|COMMIT|PRUNE|SKIP|THEN)\n (?:(:)([^)]+))?\n | (?:(LIMIT_(?:DEPTH|HEAP|MATCH))(=)(\\d+))\n | (\n CRLF | CR | ANYCRLF | ANY | LF | NUL\n | BSR_ANYCRLF | BSR_UNICODE\n | NOTEMPTY_ATSTART | NOTEMPTY\n | NO_AUTO_POSSESS | NO_DOTSTAR_ANCHOR\n | NO_JIT | NO_START_OPT | UTF | UCP\n )\n)\n(\\))", + "captures": { + "1": { "name": "keyword.control.directive.regexp" }, + "2": { "name": "keyword.control.directive.regexp" }, + "3": { "name": "keyword.control.directive.regexp" }, + "4": { "name": "variable.language.tag.regexp" }, + "5": { "name": "keyword.control.directive.regexp" }, + "6": { "name": "keyword.operator.assignment.regexp" }, + "7": { "name": "constant.numeric.integer.decimal.regexp" }, + "8": { "name": "keyword.control.directive.regexp" }, + "9": { "name": "keyword.control.directive.regexp" } + } + }, + + "literals-regular-expression-literal-callout": { + "name": "meta.callout.regexp", + "match": "(?x)\n# PCRECallout\n(\\()(?\\?C)\n (?:\n (?\\d+)\n | `(?(?:[^`]|``)*)`\n | '(?(?:[^']|'')*)'\n | \"(?(?:[^\"]|\"\")*)\"\n | \\^(?(?:[^\\^]|\\^\\^)*)\\^\n | %(?(?:[^%]|%%)*)%\n | \\#(?(?:[^#]|\\#\\#)*)\\#\n | \\$(?(?:[^$]|\\$\\$)*)\\$\n | \\{(?(?:[^}]|\\}\\})*)\\}\n )?\n(\\))\n# NamedCallout\n| (\\()(?\\*)\n (?(?!\\d)\\w+)\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?:\\{ [^,}]+ (?:,[^,}]+)* \\})?\n (\\))\n# InterpolatedCallout\n| (\\()(?\\?)\n (?>(\\{(?:\\g<-1>|(?!{).*?)\\}))\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?[X<>]?)\n (\\))", + "captures": { + "1": { "name": "punctuation.definition.group.regexp" }, + "2": { "name": "keyword.control.callout.regexp" }, + "3": { "name": "constant.numeric.integer.decimal.regexp" }, + "4": { "name": "entity.name.function.callout.regexp" }, + "5": { "name": "entity.name.function.callout.regexp" }, + "6": { "name": "entity.name.function.callout.regexp" }, + "7": { "name": "entity.name.function.callout.regexp" }, + "8": { "name": "entity.name.function.callout.regexp" }, + "9": { "name": "entity.name.function.callout.regexp" }, + "10": { "name": "entity.name.function.callout.regexp" }, + "11": { "name": "entity.name.function.callout.regexp" }, + "12": { "name": "punctuation.definition.group.regexp" }, + "13": { "name": "punctuation.definition.group.regexp" }, + "14": { "name": "keyword.control.callout.regexp" }, + "15": { "name": "entity.name.function.callout.regexp" }, + "16": { "name": "variable.language.tag-name.regexp" }, + "17": { "name": "punctuation.definition.group.regexp" }, + "18": { "name": "punctuation.definition.group.regexp" }, + "19": { "name": "keyword.control.callout.regexp" }, + "21": { "name": "variable.language.tag-name.regexp" }, + "22": { "name": "keyword.control.callout.regexp" }, + "23": { "name": "punctuation.definition.group.regexp" } + } + }, + + "literals-regular-expression-literal-character-properties": { + "name": "constant.other.character-class.set.regexp", + "match": "(?x)\n\\\\[pP]\\{ ([\\s\\w-]+(?:=[\\s\\w-]+)?) \\}\n| (\\[:) ([\\s\\w-]+(?:=[\\s\\w-]+)?) (:\\])", + "captures": { + "1": { "name": "support.variable.character-property.regexp" }, + "2": { "name": "punctuation.definition.character-class.regexp" }, + "3": { "name": "support.variable.character-property.regexp" }, + "4": { "name": "punctuation.definition.character-class.regexp" } + } + }, + + "literals-regular-expression-literal-custom-char-class": { + "patterns": [ + { + "name": "constant.other.character-class.set.regexp", + "begin": "(\\[)(\\^)?", + "end": "\\]", + "beginCaptures": { + "1": { "name": "punctuation.definition.character-class.regexp" }, + "2": { "name": "keyword.operator.negation.regexp" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.character-class.regexp" } + }, + "patterns": [ + { "include": "#literals-regular-expression-literal-custom-char-class-members" } + ] + } + ] + }, + + "literals-regular-expression-literal-custom-char-class-members": { + "comment": "TODO: should also include atoms?", + "patterns": [ + { + "comment": "\\b inside a character class represents a backspace", + "name": "constant.character.escape.backslash.regexp", + "match": "\\\\b" + }, + { "include": "#literals-regular-expression-literal-custom-char-class" }, + { "include": "#literals-regular-expression-literal-quote" }, + { "include": "#literals-regular-expression-literal-set-operators" }, + { "include": "#literals-regular-expression-literal-unicode-scalars" }, + { "include": "#literals-regular-expression-literal-character-properties" } + ] + }, + + "literals-regular-expression-literal-group-option-toggle": { + "comment": "A matching option sequence may be part of an \"isolated group\" which has an implicit scope that wraps the remaining elements of the current group", + "name": "keyword.other.option-toggle.regexp", + "match": "(?x)\n\\(\\?\n(?:\n \\^(?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})+\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})* - (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n)\n\\)" + }, + + "literals-regular-expression-literal-group-or-conditional": { + "patterns": [ + { + "name": "meta.group.absent.regexp", + "begin": "(\\()(\\?~)", + "end": "\\)", + "beginCaptures": { + "1": { "name": "punctuation.definition.group.regexp" }, + "2": { "name": "keyword.control.conditional.absent.regexp" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.group.regexp" } + }, + "patterns": [{ "include": "#literals-regular-expression-literal-regex-guts" }] + }, + + { + "name": "meta.group.conditional.regexp", + "begin": "(?x)\n# KnownConditionalStart\n(\\() (?\\?\\()\n (?:\n (? (?[+-]?\\d+)(?:(?[+-])(?\\d+))? )\n | (?R) \\g?\n | (?R&) (? (?(?!\\d)\\w+) (?:(?[+-])(?\\d+))? )\n | (?<) (?:\\g|\\g) (?>)\n | (?') (?:\\g|\\g) (?')\n | (?DEFINE)\n | (?VERSION)(?>?=)(?\\d+\\.\\d+)\n )\n(?\\))\n| (\\()(?\\?)(?=\\()", + "end": "\\)", + "beginCaptures": { + "1": { "name": "punctuation.definition.group.regexp" }, + "2": { "name": "keyword.control.conditional.regexp" }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, + "5": { "name": "keyword.operator.recursion-level.regexp" }, + "6": { "name": "constant.numeric.integer.decimal.regexp" }, + "7": { "name": "keyword.control.conditional.regexp" }, + "8": { "name": "keyword.control.conditional.regexp" }, + "10": { "name": "variable.other.group-name.regexp" }, + "11": { "name": "keyword.operator.recursion-level.regexp" }, + "12": { "name": "constant.numeric.integer.decimal.regexp" }, + "13": { "name": "keyword.control.conditional.regexp" }, + "14": { "name": "keyword.control.conditional.regexp" }, + "15": { "name": "keyword.control.conditional.regexp" }, + "16": { "name": "keyword.control.conditional.regexp" }, + "17": { "name": "keyword.control.conditional.regexp" }, + "18": { "name": "keyword.control.conditional.regexp" }, + "19": { "name": "keyword.operator.comparison.regexp" }, + "20": { "name": "constant.numeric.integer.decimal.regexp" }, + "21": { "name": "keyword.control.conditional.regexp" }, + "22": { "name": "punctuation.definition.group.regexp" }, + "23": { "name": "keyword.control.conditional.regexp" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.group.regexp" } + }, + "patterns": [{ "include": "#literals-regular-expression-literal-regex-guts" }] + }, + + { + "name": "meta.group.regexp", + "begin": "(?x)\n(\\()\n(\n # BasicGroupKind\n (\\?)\n (?:\n ([:|>=!*] | <[=!*])\n # named groups\n | P?< (?:((?!\\d)\\w+) (-))? ((?!\\d)\\w+) >\n | ' (?:((?!\\d)\\w+) (-))? ((?!\\d)\\w+) '\n # matching options\n | (?:\n \\^(?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})+\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})* - (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n ): # case without : is handled by group-option-toggle\n )\n # PCRE2GroupKind\n | \\*(\n atomic\n |pla|positive_lookahead\n |nla|negative_lookahead\n |plb|positive_lookbehind\n |nlb|negative_lookbehind\n |napla|non_atomic_positive_lookahead\n |naplb|non_atomic_positive_lookbehind\n |sr|script_run\n |asr|atomic_script_run\n ):\n)?+", + "end": "\\)", + "beginCaptures": { + "1": { "name": "punctuation.definition.group.regexp" }, + "2": { "name": "keyword.other.group-options.regexp" }, + "3": { "name": "punctuation.definition.group.regexp" }, + "4": { "name": "punctuation.definition.group.regexp" }, + "5": { "name": "variable.other.group-name.regexp" }, + "6": { "name": "keyword.operator.balancing-group.regexp" }, + "7": { "name": "variable.other.group-name.regexp" }, + "8": { "name": "variable.other.group-name.regexp" }, + "9": { "name": "keyword.operator.balancing-group.regexp" }, + "10": { "name": "variable.other.group-name.regexp" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.group.regexp" } + }, + "patterns": [{ "include": "#literals-regular-expression-literal-regex-guts" }] + } + ] + }, + + "literals-regular-expression-literal-line-comment": { + "name": "comment.line.regexp", + "match": "(\\#).*$", + "captures": { + "1": { "name": "punctuation.definition.comment.regexp" } + } + }, + + "literals-regular-expression-literal-quote": { + "name": "string.quoted.other.regexp.swift", + "begin": "\\\\Q", + "end": "\\\\E|(\\n)", + "beginCaptures": { + "0": { "name": "constant.character.escape.backslash.regexp" } + }, + "endCaptures": { + "0": { "name": "constant.character.escape.backslash.regexp" }, + "1": { "name": "invalid.illegal.returns-not-allowed.regexp" } + } + }, + + "literals-regular-expression-literal-regex-guts": { + "patterns": [ + { "include": "#literals-regular-expression-literal-quote" }, + + { + "name": "comment.block.regexp", + "begin": "\\(\\?\\#", + "end": "\\)", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.begin.regexp" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.comment.end.regexp" } + } + }, + + { + "name": "meta.embedded.expression.regexp", + "begin": "<\\{", + "end": "\\}>", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.regexp" } + }, + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.regexp" } + } + }, + + { "include": "#literals-regular-expression-literal-unicode-scalars" }, + { "include": "#literals-regular-expression-literal-character-properties" }, + + { + "name": "keyword.control.anchor.regexp", + "match": "[$^]|\\\\[AbBGyYzZ]|\\\\K" + }, + + { + "include": "#literals-regular-expression-literal-backtracking-directive-or-global-matching-option" + }, + { "include": "#literals-regular-expression-literal-callout" }, + { + "include": "#literals-regular-expression-literal-backreference-or-subpattern" + }, + + { + "name": "constant.character.character-class.regexp", + "match": "\\.|\\\\[CdDhHNORsSvVwWX]" + }, + + { + "name": "constant.character.entity.control-character.regexp", + "match": "\\\\c." + }, + + { + "name": "constant.character.escape.backslash.regexp", + "match": "\\\\[^c]" + }, + + { + "name": "keyword.operator.or.regexp", + "match": "\\|" + }, + + { + "name": "keyword.operator.quantifier.regexp", + "match": "[*+?]" + }, + + { + "name": "keyword.operator.quantifier.regexp", + "match": "\\{\\s*\\d+\\s*(?:,\\s*\\d*\\s*)?\\}|\\{\\s*,\\s*\\d+\\s*\\}" + }, + + { "include": "#literals-regular-expression-literal-custom-char-class" }, + { "include": "#literals-regular-expression-literal-group-option-toggle" }, + { "include": "#literals-regular-expression-literal-group-or-conditional" } + ] + }, + + "literals-regular-expression-literal-set-operators": { + "patterns": [ + { + "name": "keyword.operator.intersection.regexp.swift", + "match": "&&" + }, + + { + "name": "keyword.operator.subtraction.regexp.swift", + "match": "--" + }, + + { + "name": "keyword.operator.symmetric-difference.regexp.swift", + "match": "\\~\\~" + } + ] + }, + + "literals-regular-expression-literal-unicode-scalars": { + "name": "constant.character.numeric.regexp", + "match": "(?x)\n\\\\u\\{\\s*(?:[0-9a-fA-F]+\\s*)+\\}\n| \\\\u[0-9a-fA-F]{4}\n| \\\\x\\{[0-9a-fA-F]+\\}\n| \\\\x[0-9a-fA-F]{0,2}\n| \\\\U[0-9a-fA-F]{8}\n| \\\\o\\{[0-7]+\\}\n| \\\\0[0-7]{0,3}\n| \\\\N\\{(?:U\\+[0-9a-fA-F]{1,8} | [\\s\\w-]+)\\}" + }, + + "literals-string": { + "patterns": [ + { + "comment": "SE-0168: Multi-Line String Literals", + "name": "string.quoted.double.block.swift", + "begin": "\"\"\"", + "end": "\"\"\"(#*)", + "beginCaptures": { + "0": { "name": "punctuation.definition.string.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.string.end.swift" }, + "1": { "name": "invalid.illegal.extra-closing-delimiter.swift" } + }, + "patterns": [ + { + "name": "invalid.illegal.content-after-opening-delimiter.swift", + "match": "\\G.+(?=\"\"\")|\\G.+" + }, + + { + "name": "constant.character.escape.newline.swift", + "match": "\\\\\\s*\\n" + }, + + { "include": "#literals-string-string-guts" }, + + { + "comment": "Allow \\(\"\"\"...\"\"\") to appear inside a block string", + "name": "invalid.illegal.content-before-closing-delimiter.swift", + "match": "\\S((?!\\\\\\().)*(?=\"\"\")" + } + ] + }, + + { + "name": "string.quoted.double.block.raw.swift", + "begin": "#\"\"\"(?!#)(?=(?:[^\"]|\"(?!#))*$)", + "end": "\"\"\"#(#*)", + "beginCaptures": { + "0": { "name": "punctuation.definition.string.begin.swift" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.string.end.swift" }, + "1": { "name": "invalid.illegal.extra-closing-delimiter.swift" } + }, + "patterns": [ + { + "name": "invalid.illegal.content-after-opening-delimiter.swift", + "match": "\\G.+(?=\"\"\")|\\G.+" + }, + + { + "name": "constant.character.escape.newline.swift", + "match": "\\\\#\\s*\\n" + }, + + { "include": "#literals-string-raw-string-guts" }, + + { + "comment": "Allow \\(\"\"\"...\"\"\") to appear inside a block string", + "name": "invalid.illegal.content-before-closing-delimiter.swift", + "match": "\\S((?!\\\\#\\().)*(?=\"\"\")" + } + ] + }, + + { + "name": "string.quoted.double.block.raw.swift", + "begin": "(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "captures": { + "1": { "name": "variable.other.swift" }, + "2": { "name": "punctuation.definition.identifier.swift" }, + "3": { "name": "punctuation.definition.identifier.swift" } + } + } + ] + }, + + "operators": { + "patterns": [ + { + "comment": "Type casting", + "name": "keyword.operator.type-casting.swift", + "match": "\\b(is\\b|as([!?]\\B|\\b))" + }, + + { + "comment": "This rule helps us speed up the matching.", + "begin": "(?x)\n(?=\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n | \\.\n (\n \\g # operator-head\n | \\.\n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)", + "end": "(?!\\G)", + "patterns": [ + { + "comment": "Prefix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?<=^|[\\s(\\[{,;:])\n(\n (?!(//|/\\*|\\*/))\n (\n [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?![\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.increment-or-decrement.swift", + "match": "\\G(\\+\\+|\\-\\-)$" + }, + + { + "name": "keyword.operator.arithmetic.unary.swift", + "match": "\\G(\\+|\\-)$" + }, + + { + "name": "keyword.operator.logical.not.swift", + "match": "\\G!$" + }, + + { + "name": "keyword.operator.bitwise.not.swift", + "match": "\\G~$" + }, + + { + "name": "keyword.operator.custom.prefix.swift", + "match": ".+" + } + ] + } + } + }, + + { + "comment": "Postfix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?=[\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.increment-or-decrement.swift", + "match": "\\G(\\+\\+|\\-\\-)$" + }, + + { + "name": "keyword.operator.increment-or-decrement.swift", + "match": "\\G!$" + }, + + { + "name": "keyword.operator.custom.postfix.swift", + "match": ".+" + } + ] + } + } + }, + + { + "comment": "Infix operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(\n (?!(//|/\\*|\\*/))\n (\n [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.assignment.swift", + "match": "\\G=$" + }, + + { + "name": "keyword.operator.assignment.compound.swift", + "match": "\\G(\\+|\\-|\\*|/|%|<<|>>|&|\\^|\\||&&|\\|\\|)=$" + }, + + { + "name": "keyword.operator.arithmetic.swift", + "match": "\\G(\\+|\\-|\\*|/)$" + }, + + { + "name": "keyword.operator.arithmetic.overflow.swift", + "match": "\\G&(\\+|\\-|\\*)$" + }, + + { + "name": "keyword.operator.arithmetic.remainder.swift", + "match": "\\G%$" + }, + + { + "name": "keyword.operator.comparison.swift", + "match": "\\G(==|!=|>|<|>=|<=|~=)$" + }, + + { + "name": "keyword.operator.coalescing.swift", + "match": "\\G\\?\\?$" + }, + + { + "name": "keyword.operator.logical.swift", + "match": "\\G(&&|\\|\\|)$" + }, + + { + "name": "keyword.operator.bitwise.swift", + "match": "\\G(&|\\||\\^|<<|>>)$" + }, + + { + "name": "keyword.operator.bitwise.swift", + "match": "\\G(===|!==)$" + }, + + { + "name": "keyword.operator.ternary.swift", + "match": "\\G\\?$" + }, + + { + "name": "keyword.operator.custom.infix.swift", + "match": ".+" + } + ] + } + } + }, + + { + "comment": "Dot prefix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?<=^|[\\s(\\[{,;:])\n\\. # dot\n(\n (?!(//|/\\*|\\*/))\n (\n \\. # dot\n | [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?![\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.custom.prefix.dot.swift", + "match": ".+" + } + ] + } + } + }, + + { + "comment": "Dot postfix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?=[\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.custom.postfix.dot.swift", + "match": ".+" + } + ] + } + } + }, + + { + "comment": "Dot infix operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n\\. # dot\n(\n (?!(//|/\\*|\\*/))\n (\n \\. # dot\n | [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.range.swift", + "match": "\\G\\.\\.[.<]$" + }, + + { + "name": "keyword.operator.custom.infix.dot.swift", + "match": ".+" + } + ] + } + } + } + ] + }, + + { + "name": "keyword.operator.ternary.swift", + "match": ":" + } + ] + }, + + "root": { + "patterns": [ + { "include": "#compiler-control" }, + { "include": "#declarations" }, + { "include": "#expressions" } + ] + } + } +} \ No newline at end of file diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index c86abfa13e..a410aef45f 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -1,13 +1,15 @@ 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 + M306 761c3-3 8-7 16-11 15-7 35-14 60-19 4-1 14-3 11-18l-3-16c-4-18-13-14-18-13C301 699 256 724 256 763c0 58 112 91 252 91 140 0 252-33 252-91 0-38-45-64-116-78-5-1-16-3-18 13-1 6-2 11-3 15-4 18 8 18 14 19 24 5 44 11 58 18 8 4 13 8 16 11 1 2 2 2 2 1 0 3-17 15-50 24-40 12-95 19-155 19-59 0-114-7-155-19-32-9-49-21-50-24 0 0 0-0 2-2zm73-123c54 79 107 142 129 142 22 0 75-62 129-142 62-91 101-173 101-231C738 280 635 176 508 176c-127 0-230 104-230 232 0 57 39 139 101 231zm150 64a446 446 0 01-21 23c-6-6-13-14-21-23-21-25-46-57-68-91-57-84-92-159-92-204 0-102 82-184 182-184 100 0 182 82 182 184 0 45-36 120-92 204-23 33-47 66-68 91zm-21-200a102 102 0 100-204 102 102 0 000 204zm0-48a54 54 0 110-108 54 54 0 010 108z 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 + 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 @@ -22,6 +24,7 @@ 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 @@ -32,6 +35,8 @@ 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 + M925 386c-23-53-55-101-96-142-41-41-89-73-142-96C631 124 573 112 512 112s-119 12-174 35c-53 23-101 55-142 96-41 41-73 89-96 142C76 441 64 500 64 560c0 133 58 258 160 343l2 1c6 5 13 8 21 8h532c8 0 15-3 21-8l2-1C902 818 960 693 960 560c0-61-12-119-35-174zM761 836H263C185 766 140 666 140 560c0-99 39-193 109-263 70-70 164-109 263-109 99 0 193 39 263 109 70 70 109 164 109 263 0 106-45 206-123 276zM624 422c-3-3-8-3-11 0L528 506c-19-5-39-0-54 15-22 22-22 57 0 79 22 22 57 22 79 0 15-15 20-35 15-54l85-85c3-3 3-8 0-11l-28-28zM490 320h44c4 0 8-4 8-8v-80c0-4-4-8-8-8h-44c-4 0-8 4-8 8v80c0 4 4 8 8 8zM750 538v44c0 4 4 8 8 8h80c4 0 8-4 8-8v-44c0-4-4-8-8-8h-80c-4 0-8 4-8 8zM763 341l-31-31c-3-3-8-3-11 0l-57 57c-3 3-3 8 0 11l31 31c3 3 8 3 11 0l57-57c3-3 3-8 0-11zM304 310c-3-3-8-3-11 0l-31 31c-3 3-3 8 0 11l57 57c3 3 8 3 11 0l31-31c3-3 3-8 0-11l-57-57zM262 530h-80c-4 0-8 4-8 8v44c0 4 4 8 8 8h80c4 0 8-4 8-8v-44c0-4-4-8-8-8z + 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 @@ -50,7 +55,9 @@ 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 + M725 768c24 0 43 19 43 43s-19 43-43 43-43-19-43-43 19-43 43-43zm0-128c-116 0-216 71-256 171 40 100 140 171 256 171s216-71 256-171c-40-100-140-171-256-171zm0 277a107 107 0 110-213 107 107 0 010 213zM396 853H256V171h299v213h213v174c30 3 58 11 85 21V341l-256-256H256a85 85 0 00-85 85v683a85 85 0 0085 85h192c-21-25-39-54-52-85z 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 M320 239 213 299l60-107L213 85l107 60L427 85 367 192 427 299 320 239m512 418L939 597l-60 107L939 811l-107-60L725 811l60-107L725 597l107 60M939 85l-60 107L939 299l-107-60L725 299l60-107L725 85l107 60L939 85m-369 460 104-104-90-90-104 104 90 90m44-234 100 100c17 16 17 44 0 60L215 969c-17 17-44 17-60 0l-100-100c-17-16-17-44 0-60L553 311c17-17 44-17 60 0z @@ -58,18 +65,16 @@ 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 M0 512M1024 512M512 0M512 1024M955 323q0 23-16 39l-414 414-78 78q-16 16-39 16t-39-16l-78-78-207-207q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 169 375-375q16-16 39-16t39 16l78 78q16 16 16 39z M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z M24 512A488 488 0 01512 24A488 488 0 011000 512A488 488 0 01512 1000A488 488 0 0124 512zm447-325v327L243 619l51 111 300-138V187H471z - M832 64h128v278l-128-146V64zm64 448L512 73 128 512H0L448 0h128l448 512h-128zm0 83V1024H640V704c0-35-29-64-64-64h-128a64 64 0 00-64 64v320H128V595l384-424 384 424z M512 0C229 0 0 229 0 512c0 283 229 512 512 512s512-229 512-512c0-283-229-512-512-512zm0 958C266 958 66 758 66 512S266 66 512 66 958 266 958 512 758 958 512 958zM192 416h96a32 32 0 0032-32v-32a32 32 0 00-32-32H192a32 32 0 00-32 32v32a32 32 0 0032 32zM384 416h96a32 32 0 0032-32v-32a32 32 0 00-32-32h-96a32 32 0 00-32 32v32a32 32 0 0032 32zM576 416h96a32 32 0 0032-32v-32a32 32 0 00-32-32h-96a32 32 0 00-32 32v32a32 32 0 0032 32zM832 320h-64a32 32 0 00-32 32v128h-160a32 32 0 00-32 32v32a32 32 0 0032 32h256a32 32 0 0032-32v-192a32 32 0 00-32-32zM320 544v-32a32 32 0 00-32-32H192a32 32 0 00-32 32v32a32 32 0 0032 32h96a32 32 0 0032-32zM384 576h96a32 32 0 0032-32v-32a32 32 0 00-32-32h-96a32 32 0 00-32 32v32a32 32 0 0032 32zM800 640H256a32 32 0 00-32 32v32a32 32 0 0032 32h544a32 32 0 0032-32v-32a32 32 0 00-32-32z M973 358a51 51 0 0151 51v563a51 51 0 01-51 51H51a51 51 0 01-51-51V410a51 51 0 0151-51h256a51 51 0 110 102H102v461h819V461h-205a51 51 0 110-102h256zM51 102a51 51 0 110-102h256c141 0 256 115 256 256v388l66-66a51 51 0 1172 72l-154 154a51 51 0 01-72 0l-154-154a51 51 0 1172-72L461 644V256c0-85-69-154-154-154H51z M512 0C229 0 0 229 0 512s229 512 512 512 512-229 512-512S795 0 512 0zM512 928c-230 0-416-186-416-416S282 96 512 96s416 186 416 416S742 928 512 928zM538 343c47 0 83-38 83-78 0-32-21-61-62-61-55 0-82 45-82 77C475 320 498 343 538 343zM533 729c-8 0-11-10-3-40l43-166c16-61 11-100-22-100-39 0-131 40-211 108l16 27c25-17 68-35 78-35 8 0 7 10 0 36l-38 158c-23 89 1 110 34 110 33 0 118-30 196-110l-19-25C575 717 543 729 533 729z @@ -91,11 +96,13 @@ 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 @@ -108,7 +115,6 @@ 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 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 - m224 154a166 166 0 00-166 166v192a166 166 0 00166 166h64v-76h-64a90 90 0 01-90-90v-192a90 90 0 0190-90h320a90 90 0 0190 90v192a90 90 0 01-90 90h-128v77h128a166 166 0 00166-167v-192a166 166 0 00-166-166h-320zm166 390a90 90 0 0190-90h128v-76h-128a166 166 0 00-166 166v192a166 166 0 00166 166h320a166 166 0 00166-166v-192a166 166 0 00-166-166h-64v77h64a90 90 0 0190 90v192a90 90 0 01-90 90h-320a90 90 0 01-90-90v-192z M512 128M706 302a289 289 0 00-173 44 27 27 0 1029 46 234 234 0 01125-36c23 0 45 3 66 9 93 28 161 114 161 215C914 704 813 805 687 805H337C211 805 110 704 110 580c0-96 61-178 147-210C282 263 379 183 495 183a245 245 0 01210 119z M364 512h67v108h108v67h-108v108h-67v-108h-108v-67h108v-108zm298-64A107 107 0 01768 555C768 614 720 660 660 660h-108v-54h-108v-108h-94v108h-94c4-21 22-47 44-51l-1-12a75 75 0 0171-75a128 128 0 01239-7a106 106 0 0153-14z 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 @@ -121,12 +127,15 @@ 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 + M360 623c-32 0-62 11-89 32s-46 48-54 79H94c-8 3-15 7-20 14s-9 15-10 26c-1 11 2 19 10 26s17 10 28 10h115c8 32 26 58 54 79 28 21 58 32 91 32s64-11 91-32c28-21 46-48 54-79h421c8-3 15-7 22-14 7-7 10-15 10-24s-4-18-12-26c-8-8-17-12-28-12H507c-8-32-26-58-54-79s-58-32-89-32h-4zm0 75c21 0 39 7 54 22 15 15 22 32 22 52s-7 37-22 52c-15 15-32 22-52 22-20 0-37-7-52-22s-22-32-22-52 7-37 22-52 32-22 54-22h-4zM662 103c-34 0-66 11-93 32-28 21-46 48-54 79H94c-8 3-15 7-20 14s-8 15-8 24 3 18 10 26c7 8 15 12 26 12h413c8 32 26 58 54 79 28 21 58 32 91 32 33 0 64-11 91-32s46-48 54-79h123c8-3 15-7 22-14s10-15 10-26-4-19-12-26-17-10-28-10H805c-8-32-26-58-54-79s-58-32-89-32zm-4 75c21 0 39 7 54 22 15 15 22 32 22 52s-7 37-22 52c-15 15-32 22-52 22-20 0-37-7-52-22s-22-32-22-52 7-37 22-52 32-22 54-22h-4z + M864 277a117 117 0 00-117-117h-469A117 117 0 00160 277v469a117 117 0 00117 117h469a117 117 0 00117-117v-469zm64 469a181 181 0 01-181 181h-469a181 181 0 01-181-181v-469A181 181 0 01277 96h469a181 181 0 01181 181v469zM256 320a64 64 0 11128 0v384a64 64 0 01-128 0v-384z + M858 213a47 47 0 00-47-47H422v691h388a47 47 0 0047-47V213zm77 597a124 124 0 01-124 124H213A124 124 0 0190 811V213c0-68 55-124 124-124h597c68 0 124 55 124 124v597z + 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 @@ -135,12 +144,14 @@ M1097 372h-460l-146-299H146a73 73 0 00-73 73v731a73 73 0 0073 73h878a73 73 0 0073-73V372zM146 0h390l146 299h488V878a146 146 0 01-146 146H146a146 146 0 01-146-146V146a146 146 0 01146-146zm439 0h195l146 246h-195l-146-246zm244 0h195a146 146 0 01146 146v100h-195l-146-246z M177 156c-22 5-33 17-36 37c-10 57-33 258-13 278l445 445c23 23 61 23 84 0l246-246c23-23 23-61 0-84l-445-445C437 120 231 145 177 156zM331 344c-26 26-69 26-95 0c-26-26-26-69 0-95s69-26 95 0C357 276 357 318 331 344z M683 537h-144v-142h-142V283H239a44 44 0 00-41 41v171a56 56 0 0014 34l321 321a41 41 0 0058 0l174-174a41 41 0 000-58zm-341-109a41 41 0 110-58a41 41 0 010 58zM649 284V142h-69v142h-142v68h142v142h69v-142h142v-68h-142z - M996 452 572 28A96 96 0 00504 0H96C43 0 0 43 0 96v408a96 96 0 0028 68l424 424c37 37 98 37 136 0l408-408c37-37 37-98 0-136zM224 320c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm1028 268L844 996c-37 37-98 37-136 0l-1-1L1055 647c34-34 53-79 53-127s-19-93-53-127L663 0h97a96 96 0 0168 28l424 424c37 37 37 98 0 136z + M43 152A109 109 0 01152 43h220c48 0 109 25 143 59L950 537a109 109 0 010 154L691 949a109 109 0 01-154 0L102 514C68 481 43 419 43 371v-220zm85 0v220c0 25 16 65 34 83l435 435a24 24 0 0033 0l259-259c9-9 9-24 0-33L454 162C436 144 397 128 371 128h-220C138 128 128 138 128 152zM277 341a64 64 0 110-128 64 64 0 010 128z + M256 256q0-30-21-52t-52-21-52 21-21 52 21 52 52 21 52-21 21-52zM866 585q0 30-21 51l-281 281q-22 21-52 21-30 0-51-21l-409-409q-22-21-37-58t-15-67l0-238q0-30 22-51t51-22l238 0q30 0 67 15t58 37l409 408q21 22 21 52zM1085 585q0 30-21 51l-281 281q-22 21-52 21-21 0-34-8t-30-26l269-269q21-21 21-51 0-30-21-52l-409-408q-22-22-58-37t-67-15l128 0q30 0 67 15t58 37l409 408q21 22 21 52z M765 118 629 239l-16 137-186 160 54 59 183-168 144 4 136-129 47-43-175-12L827 67zM489 404c-66 0-124 55-124 125s54 121 124 121c66 0 120-55 120-121H489l23-121c-8-4-16-4-23-4zM695 525c0 114-93 207-206 207s-206-94-206-207 93-207 206-207c16 0 27 0 43 4l43-207c-27-4-54-8-85-8-229 0-416 188-416 419s187 419 416 419c225 0 408-180 416-403v-12l-210-4z M144 112h736c18 0 32 14 32 32v736c0 18-14 32-32 32H144c-18 0-32-14-32-32V144c0-18 14-32 32-32zm112 211v72a9 9 0 003 7L386 509 259 615a9 9 0 00-3 7v72a9 9 0 0015 7L493 516a9 9 0 000-14l-222-186a9 9 0 00-15 7zM522 624a10 10 0 00-10 10v60a10 10 0 0010 10h237a10 10 0 0010-10v-60a10 10 0 00-10-10H522z + M929 201c-1-1-1-2-2-3-6-6-15-6-21 0L709 395l-78-78L828 120c1-1 2-2 2-3 4-7 2-16-5-20-112-66-259-51-356 45-77 77-102 185-76 283l-335 335c-3 3-3 9 0 13l194 194c4 4 9 4 13 0l335-335c98 26 206 1 283-76 96-97 112-244 45-356zM825 498c-66 66-166 79-245 38l-10 10-0-0-313 313-91-91 263-263s0 0 0 0l60-60c-41-79-28-179 38-245a211 211 0 01165-61L541 291a37 37 0 000 52l142 142a37 37 0 0052 0l152-152c4 59-16 120-61 164z M170 831 513 489 855 831 960 726 512 278 64 726 170 831zM512 278h448v-128h-896v128h448z M897 673v13c0 51-42 93-93 93h-10c-1 0-2 0-2 0H220c-23 0-42 19-42 42v13c0 23 19 42 42 42h552c14 0 26 12 26 26 0 14-12 26-26 26H220c-51 0-93-42-93-93v-13c0-51 42-93 93-93h20c1-0 2-0 2-0h562c23 0 42-19 42-42v-13c0-11-5-22-13-29-8-7-17-11-28-10H660c-14 0-26-12-26-26 0-14 12-26 26-26h144c24-1 47 7 65 24 18 17 29 42 29 67zM479 98c-112 0-203 91-203 203 0 44 14 85 38 118l132 208c15 24 50 24 66 0l133-209c23-33 37-73 37-117 0-112-91-203-203-203zm0 327c-68 0-122-55-122-122s55-122 122-122 122 55 122 122-55 122-122 122z - M912 800a48 48 0 1 1 0 96h-416a48 48 0 1 1 0-96h416z m-704-704A112 112 0 0 1 256 309.184V480h80a48 48 0 0 1 0 96H256v224h81.664a48 48 0 1 1 0 96H256a96 96 0 0 1-96-96V309.248A112 112 0 0 1 208 96z m704 384a48 48 0 1 1 0 96h-416a48 48 0 0 1 0-96h416z m0-320a48 48 0 1 1 0 96h-416a48 48 0 0 1 0-96h416z + M192 320a64 64 0 0 1-64-64V128a64 64 0 0 1 64-64h256a64 64 0 0 1 64 64v128a64 64 0 0 1-64 64H352v160H512V448a64 64 0 0 1 64-64h256a64 64 0 0 1 64 64v128a64 64 0 0 1-64 64H576a64 64 0 0 1-64-64v-32H352v256H512V768a64 64 0 0 1 64-64h256a64 64 0 0 1 64 64v128a64 64 0 0 1-64 64H576a64 64 0 0 1-64-64v-32H352a64 64 0 0 1-64-64V320H192z M30 0 30 30 0 15z M0 0 0 30 30 15z M762 1024C876 818 895 504 448 514V768L64 384l384-384v248c535-14 595 472 314 776z diff --git a/src/Resources/Images/ExternalToolIcons/JetBrains/AI.png b/src/Resources/Images/ExternalToolIcons/JetBrains/AI.png new file mode 100644 index 0000000000..64390ed327 Binary files /dev/null and b/src/Resources/Images/ExternalToolIcons/JetBrains/AI.png differ diff --git a/src/Resources/Images/ExternalToolIcons/JetBrains/IC.png b/src/Resources/Images/ExternalToolIcons/JetBrains/IC.png new file mode 100644 index 0000000000..62e50e936d Binary files /dev/null and b/src/Resources/Images/ExternalToolIcons/JetBrains/IC.png differ diff --git a/src/Resources/Images/ExternalToolIcons/JetBrains/IU.png b/src/Resources/Images/ExternalToolIcons/JetBrains/IU.png new file mode 100644 index 0000000000..62e50e936d Binary files /dev/null and b/src/Resources/Images/ExternalToolIcons/JetBrains/IU.png differ diff --git a/src/Resources/Images/ShellIcons/cmux.png b/src/Resources/Images/ShellIcons/cmux.png new file mode 100644 index 0000000000..b847bbb70f 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 0000000000..e16b37400e 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 0000000000..e2ce5897fe 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 d4ecd50b90..9e2d348bc8 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -16,21 +16,20 @@ 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 AI-Assistent Modell NEU GENERIEREN Verwende AI, um Commit-Nachrichten zu generieren + Übernehmen SourceGit minimieren + Andere ausblenden Alles anzeigen - Patch - Patch-Datei: - Wähle die anzuwendende .patch-Datei + 3-Way Merge + Wähle die anzuwendende patch-Datei Ignoriere Leerzeichen-Änderungen + Quelle: + Datei + Zwischenablage Patch anwenden Leerzeichen: Stash anwenden @@ -48,14 +47,15 @@ KEINE ALS UNVERÄNDERT ANGENOMMENEN DATEIEN 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. Einen anderen Commit auschecken und als gut oder schlecht markieren. + Bisecting. Aktuellen Commit als schlecht markieren oder einen anderen auschecken und als schlecht markieren. + 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 @@ -64,6 +64,7 @@ Ausgewählte 2 Branches vergleichen Vergleichen mit… Vergleichen mit HEAD + Vergleichen mit ${0}$ Branch-Namen kopieren PR erstellen… PR für Upstream ${0}$ erstellen… @@ -109,15 +110,18 @@ Submodul: URL: Branch auschecken - Commit auschecken - Commit: - Warnung: Durch Auschecken eines Commits wird dein HEAD losgelöst (detached)! Lokale Änderungen: Branch: 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: + Branch aus Stash auschecken + Neuer Branch: + Stash: + Commit oder Tag auschecken + Ziel: + Achtung: Danach ist HEAD losgelöst (‚detached‘) Cherry Pick Quelle an Commit-Nachricht anhängen Commit(s): @@ -129,6 +133,8 @@ Remote-Repository klonen Extra-Parameter: Zusätzliche Argumente für das Klonen des Repositorys. Optional. + Lesezeichen: + Gruppe: Lokaler Name: Repository-Name. Optional. Übergeordnetes Verzeichnis: @@ -136,18 +142,23 @@ Repository-URL: SCHLIESSEN Editor + Branches + Branches & Tags + Benutzerdefinierte Repository-Aktionen + Dateien der Revision Commit auschecken Diesen Commit cherry-picken Mehrere cherry-picken… Vergleiche mit HEAD Vergleiche mit Worktree Autor + Autor-Zeitpunkt Commit-Nachricht Committer + Committer-Zeitpunkt SHA Betreff Benutzerdefinierte Aktion - Commit entfernen Interaktives Rebase Entfernen… Bearbeiten… @@ -155,26 +166,22 @@ Interaktives Rebase von ${0}$ auf ${1}$ Umformulieren… Squash in den Vorgänger… - Merge in ${0}$ hinein 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 ÄNDERUNGEN geänderte Datei(en) Änderungen durchsuchen… + Details einklappen DATEIEN LFS-DATEI Dateien durchsuchen… Submodul INFORMATION AUTOR - NACHFOLGER COMMITTER Prüfe Refs, die diesen Commit enthalten COMMIT ENTHALTEN IN @@ -192,7 +199,11 @@ Commit-Nachricht eingeben. Leerzeile zum Trennen von Betreff und Beschreibung verwenden! BETREFF Vergleichen - Vergleichen mit HEAD + ÄNDERUNGEN + COMMITS + NUR LINKS VORHANDENE COMMITS + NUR RECHTS VORHANDENE COMMITS + Tipp: Wenn die andere Seite HEAD ist, kannst du ausgewählte Commits über das Kontextmenü cherry-picken. Repository-Einstellungen COMMIT-VORLAGE Vordefinierte Parameter: @@ -237,6 +248,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Minute(n) Konventionelle Commit-Typen Standard-Remote + `--recursive` beim Auto-Aktualisieren von Submodulen verwenden Bevorzugter Merge-Modus TICKETSYSTEM Beispiel für Azure-DevOps-Regel hinzufügen @@ -269,8 +281,11 @@ $1, $2, … Werte der Eingabe-Steuerelemente Bezeichnung: Einträge: Nutze ‚|‘, um Einträge zu trennen + String-Formatierung: + Optional. Formatiert die Ausgabe. Wird ignoriert, wenn die Eingabe leer ist. Verwende bitte `${VALUE}` als Platzhalter für die Eingabe. Die vordefinierten Parameter ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE} und ${TAG} bleiben hier verwendbar Typ: + Kurznamen verwenden: Arbeitsumgebungen Farbe Name @@ -278,6 +293,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente WEITER Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty)? ALLES STAGEN & COMMITTEN + AUSGEWÄHLTE STAGEN & COMMITTEN Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty) oder alle Änderungen stagen und dann committen? Neustart erforderlich Du musst dieses Programm neustarten, um die Änderungen zu übernehmen. @@ -290,6 +306,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Typ der Änderung: Kopieren Gesamten Text kopieren + Als Patch kopieren Ganzen Pfad kopieren Pfad kopieren Branch erstellen… @@ -322,6 +339,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Submodul: Branch löschen Branch: + Löschen erzwingen, auch bei nicht gemergten Commits Du bist dabei, einen Remote-Branch zu löschen!!! Auch Remote-Branch ${0}$ löschen Mehrere Branches löschen @@ -343,7 +361,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Tag: Von Remote-Repositorys löschen BINÄRER VERGLEICH - Dateimodus geändert + LEERE DATEI Erster Unterschied Ignoriere Leerzeichen-Änderungen ÜBEREINANDER @@ -363,6 +381,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente SUBMODUL GELÖSCHT NEU + + {0} nicht committete Änderungen Seiten tauschen Syntax-Hervorhebung Zeilenumbruch @@ -379,12 +398,10 @@ $1, $2, … Werte der Eingabe-Steuerelemente Alle Änderungen in der Arbeitskopie. Änderungen: Ignorierte Dateien einbeziehen + Geänderte/gelöschte Dateien einbeziehen Nicht-verfolgte Dateien einbeziehen {0} Änderungen werden verworfen Du kannst das nicht rückgängig machen!!! - Commit entfernen - Commit: - Neuer HEAD: Beschreibung des Branches bearbeiten Ziel-Branch: Lesezeichen: @@ -417,14 +434,25 @@ $1, $2, … Werte der Eingabe-Steuerelemente Dateiverlauf ÄNDERUNG INHALT + Dateimodus-Änderung: + Gelöschter Dateimodus: + Verzeichnis + Ausführbar + Neuer Dateimodus: + Normal + Submodul + Symlink + Unbekannt Git Flow Development-Branch: Feature: Feature-Präfix: + Abschließen ${0}$ FLOW – Feature fertigstellen FLOW – Hotfix fertigstellen FLOW – Release fertigstellen Ziel: + Vor dem Mergen rebasen Squash beim Merge Hotfix: Hotfix-Präfix: @@ -433,10 +461,12 @@ $1, $2, … Werte der Eingabe-Steuerelemente Production-Branch: Release: Release-Präfix: + Beginnen bei: Feature beginnen… FLOW – Feature beginnen Hotfix beginnen… FLOW – Hotfix beginnen + Name: Namen eingeben Release beginnen… FLOW – Release beginnen @@ -470,18 +500,25 @@ $1, $2, … Werte der Eingabe-Steuerelemente Remote: Verfolge alle ‚{0}‘ Dateien Verfolge alle *{0} Dateien - VERLAUF + Commit auswählen AUTOR AUTOR-ZEITPUNKT COMMIT-ZEITPUNKT - COMMIT-ZEITPUNKT VERLAUF & COMMIT-NACHRICHT SHA + HERVORHEBUNGEN IM GRAPHEN + Alle + Nur aktueller Branch + Aktueller Branch & ausgewählte Commits + Nur ausgewählte Commits {0} COMMITS AUSGEWÄHLT SPALTEN ANZEIGEN Halte ‚Strg‘ oder ‚Umschalt‘, um mehrere Commits auszuwählen. Halte ⌘ oder ⇧, um mehrere Commits auszuwählen TIPPS: + In separatem Fenster öffnen + Commit-Details + Revisionsvergleich Tastaturkürzel-Referenz GLOBAL Klone neues Repository @@ -489,6 +526,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Zur nächsten Registerkarte wechseln Zur vorherigen Registerkarte wechseln Neue Registerkarte erstellen + Lokales Repository öffnen Einstellungen öffnen Auswahlmenü für Arbeitsumgebungen anzeigen Aktive Registerkarte wechseln @@ -497,27 +535,22 @@ $1, $2, … Werte der Eingabe-Steuerelemente Gestagte Änderungen committen Gestagte Änderungen committen und pushen Alle Änderungen stagen und committen + Neuen Branch erstellen Fetch, wird direkt ausgeführt - Dashboard-Modus (Standard) + Springe zum Nachfolger des ausgewählten Commits 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 - Suchleiste schließen - Suche nächste Übereinstimmung - Suche vorherige Übereinstimmung - Öffne mit externem Diff- / Merge-Tool - Öffne Suchleiste Verwerfen Stagen Unstagen Initialisiere Repository + Möchtest du `git init` in diesem Pfad ausführen? + Öffnen des Repositorys fehlgeschlagen. Grund: Pfad: Cherry-Pick wird durchgeführt. Verarbeite Commit @@ -529,6 +562,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Reverte Commit Interaktiver Rebase Lokale Änderungen stashen & wieder anwenden + pre-rebase Hook überspringen Auf: Drag & Drop zum Anordnen der Commits Ziel-Branch: @@ -537,6 +571,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Befehle FEHLER INFO + Neue Version verfügbar Repositorys öffnen Registerkarten Arbeitsumgebungen @@ -545,6 +580,10 @@ $1, $2, … Werte der Eingabe-Steuerelemente Ziel-Branch: Merge-Option: Quelle: + Merge wird geprüft… + Merge ist ohne Konflikte möglich + Unbekannter Fehler bei der Merge-Prüfung + Merge wird Konflikte verursachen Zuerst meine, dann deren Zuerst deren, dann meine BEIDE VERWENDEN @@ -556,7 +595,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente ERGEBNIS SPEICHERN & STAGEN DEREN - Merge-Konflikt – {0} + Merge-Konflikte Ungespeicherte Änderungen verwerfen? MEINE VERWENDEN DEREN VERWENDEN @@ -578,6 +617,10 @@ $1, $2, … Werte der Eingabe-Steuerelemente Programmdaten-Ordner öffnen Datei öffnen Öffnen in externem Merge-Tool + Lokales Repository öffnen + Lesezeichen: + Gruppe: + Ordner: Optional. Neue Registerkarte erstellen Registerkarte schließen @@ -601,7 +644,10 @@ $1, $2, … Werte der Eingabe-Steuerelemente Gestern Einstellungen AI + Zusätzlicher Prompt (Verwende `-`, um deine Anforderungen aufzulisten) API-Schlüssel + Modell + Verfügbare Modell-IDs automatisch abrufen Name Der eingegebene Wert ist der Name der Umgebungsvariable, aus der der API-Schlüssel gelesen wird Server @@ -631,12 +677,13 @@ $1, $2, … Werte der Eingabe-Steuerelemente Aktiviere kompakte Ordner im Änderungsbaum Sprache Commit-Historie - Zeige Autor-Zeitpunkt anstatt Commit-Zeitpunkt Standardmäßig ‚ÄNDERUNGEN‘-Ansicht anzeigen Standardmäßig Registerkarte ‚ÄNDERUNGEN‘ in Commit-Details anzeigen - Zeige Nachfolger in den Commit-Details + Relative Zeitangaben im Commit-Graphen anzeigen Zeige Tags im Commit-Verlauf Längenvorgabe für Commit-Nachrichten + 24-Stunden-Format + Kompakte Branch-Namen im Commit-Graphen verwenden Standard-Avatar im GitHub-Stil generieren GIT Aktiviere Auto-CRLF @@ -651,6 +698,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente git-credential-libsecret statt git-credential-manager verwenden Benutzername Globaler Git-Benutzername + Änderungen beim Auschecken oder Mergen von Branches standardmäßig stashen & wieder anwenden Git-Version GPG-SIGNIERUNG Commit-Signierung @@ -682,6 +730,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Erzwinge Push Lokaler Branch: NEU + pre-push Hook überspringen Remote: Revision: Revision zu Remote-Branch pushen @@ -698,7 +747,12 @@ $1, $2, … Werte der Eingabe-Steuerelemente Schließen Aktuellen Branch rebasen Lokale Änderungen stashen & wieder anwenden + pre-rebase Hook überspringen Auf: + Rebase wird geprüft… + Rebase ist ohne Konflikte möglich + Unbekannter Fehler bei der Rebase-Prüfung + Rebase wird Konflikte verursachen Remote hinzufügen Remote bearbeiten Name: @@ -709,6 +763,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Benutzerdefinierte Aktion Löschen… Bearbeiten… + Auto-Fetch aktivieren Fetch Im Browser öffnen Prune @@ -732,14 +787,16 @@ $1, $2, … Werte der Eingabe-Steuerelemente WEITER Benutzerdefinierte Aktionen Keine benutzerdefinierten Aktionen - Dashboard Alle Änderungen verwerfen Öffne im Datei-Browser Suche Branches / Tags / Submodule Sichtbarkeit im Verlauf + Filter einklappen Aufheben Im Verlauf ausblenden + Filter ausklappen Im Verlauf filtern + Filter aktiv LAYOUT Horizontal Vertikal @@ -747,11 +804,9 @@ $1, $2, … Werte der Eingabe-Steuerelemente Commit-Zeitpunkt Topologisch LOKALE BRANCHES - Mehr Optionen… Zum HEAD wechseln Erstelle Branch BENACHRICHTIGUNGEN LÖSCHEN - Nur aktuellen Branch hervorheben Als Ordner öffnen Öffne in {0} Öffne in externen Tools @@ -760,7 +815,6 @@ $1, $2, … Werte der Eingabe-Steuerelemente AUFLÖSEN Commit suchen Autor - Committer Inhalt Commit-Nachricht Pfad @@ -783,7 +837,6 @@ $1, $2, … Werte der Eingabe-Steuerelemente Nach Name Sortieren Öffne im Terminal - Verwende relative Zeitangaben Logs ansehen Öffne ‚{0}‘ im Browser WORKTREES @@ -801,7 +854,6 @@ $1, $2, … Werte der Eingabe-Steuerelemente Commit rückgängig machen Commit: Commit-Änderungen rückgängig machen - Commit-Nachricht umformulieren In Bearbeitung. Bitte warten… SPEICHERN Speichern als… @@ -829,9 +881,6 @@ $1, $2, … Werte der Eingabe-Steuerelemente Upstream: SHA kopieren Zum Commit wechseln - Squash HEAD in Vorgänger - Fixup HEAD in Vorgänger - Ziel: Privater SSH-Schlüssel: Pfad zum privaten SSH-Schlüssel START @@ -844,6 +893,8 @@ $1, $2, … Werte der Eingabe-Steuerelemente Gestagte und ungestagte Änderungen der ausgewählten Datei(en) werden gestasht!!! Lokale Änderungen stashen Anwenden + Änderungen anwenden + Neuen Branch auschecken Kopiere Nachricht Entfernen Als Patch speichern… @@ -880,9 +931,12 @@ $1, $2, … Werte der Eingabe-Steuerelemente nicht gemerged Aktualisieren URL + Details der Submodul-Änderung + DETAILS ÖFFNEN OK TAGGER ZEIT + Tag auschecken… Vergleiche 2 Tags Vergleichen mit… Vergleichen mit HEAD @@ -899,6 +953,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Alle Submodule Bei Bedarf initialisieren Submodul: + Verschachtelte Submodule aktualisieren Auf verfolgten Remote-Branch des Submoduls aktualisieren URL: Logs @@ -925,6 +980,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente Ignoriere *{0} Dateien im selben Ordner Ignoriere nicht-verfolgte Dateien in diesem Ordner Ignoriere nur diese Datei + Ignoriere alle nicht-verfolgten Dateien im selben Ordner Nachbesserung Du kannst diese Datei jetzt stagen. Verlauf löschen @@ -944,6 +1000,7 @@ $1, $2, … Werte der Eingabe-Steuerelemente DATEI-KONFLIKTE SIND GELÖST MEINE VERSION VERWENDEN DEREN VERSION VERWENDEN + Änderungen filtern… NICHT-VERFOLGTE DATEIEN EINBEZIEHEN KEINE BISHERIGEN COMMIT-NACHRICHTEN KEINE COMMIT-VORLAGEN @@ -961,9 +1018,12 @@ $1, $2, … Werte der Eingabe-Steuerelemente ARBEITSUMGEBUNG: Arbeitsumgebungen konfigurieren… WORKTREE + BRANCH Pfad kopieren + HEAD Sperren Öffnen + PFAD Entfernen Entsperren JA diff --git a/src/Resources/Locales/el_GR.axaml b/src/Resources/Locales/el_GR.axaml new file mode 100644 index 0000000000..8e5e5f4dfe --- /dev/null +++ b/src/Resources/Locales/el_GR.axaml @@ -0,0 +1,1025 @@ + + + + + + Σχετικά + Σχετικά με το SourceGit + Ημερομηνία έκδοσης: {0} + Σημειώσεις έκδοσης + Ελεύθερο & ανοιχτού κώδικα γραφικό περιβάλλον για το Git + Προσθήκη αρχείου(-ων) στα αγνοούμενα + Μοτίβο: + Αρχείο αποθήκευσης: + Προσθήκη Worktree + Τοποθεσία: + Διαδρομή για αυτό το worktree. Υποστηρίζεται σχετική διαδρομή. + Όνομα κλάδου: + Προαιρετικό. Προεπιλογή είναι το όνομα του φακέλου προορισμού. + Βοηθός 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 + Πρόκειται να διαγράψετε έναν απομακρυσμένο κλάδο!!! + Διαγραφή και του απομακρυσμένου κλάδου ${0}$ + Διαγραφή πολλαπλών κλάδων + Προσπαθείτε να διαγράψετε πολλούς κλάδους ταυτόχρονα. Βεβαιωθείτε ότι ελέγξατε ξανά πριν προχωρήσετε! + Διαγραφή πολλαπλών ετικετών + Διαγραφή τους από τα απομακρυσμένα + Προσπαθείτε να διαγράψετε πολλές ετικέτες ταυτόχρονα. Βεβαιωθείτε ότι ελέγξατε ξανά πριν προχωρήσετε! + Διαγραφή απομακρυσμένου + Απομακρυσμένο: + Διαδρομή: + Στόχος: + Όλα τα παιδιά θα αφαιρεθούν από τη λίστα. + Αυτό θα το αφαιρέσει μόνο από τη λίστα, όχι από τον δίσκο! + Επιβεβαίωση διαγραφής ομάδας + Επιβεβαίωση διαγραφής αποθετηρίου + Διαγραφή υπομονάδας + Διαδρομή υπομονάδας: + Διαγραφή ετικέτας + Ετικέτα: + Διαγραφή από τα απομακρυσμένα αποθετήρια + ΔΥΑΔΙΚΟ 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 + Άνοιγμα παλέτας εντολών + 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 + Τοπικός κλάδος: + ΝΕΟ + Παράκαμψη του pre-push hook + Απομακρυσμένο: + Αναθεώρηση: + 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 5d5b4a90bf..76ac9e4dcb 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1,6 +1,5 @@ About - Source Revision: About SourceGit Release Date: {0} Release Notes @@ -13,11 +12,6 @@ Path for this worktree. Relative path is supported. Branch Name: Optional. Default is the destination folder name. - Track Branch: - Tracking remote branch - What to Checkout: - Create New Branch - Existing Branch AI Assistant MODEL RE-GENERATE @@ -26,11 +20,12 @@ Hide SourceGit Hide Others Show All - Patch 3-Way Merge - Patch File: - Select .patch file to apply + Select patch file to apply Ignore whitespace changes + Source: + File + Clipboard Apply Patch Whitespace: Apply Stash @@ -48,38 +43,42 @@ NO FILES ASSUMED AS UNCHANGED Load Image... Refresh - BINARY FILE NOT SUPPORTED!!! Bisect Abort Bad - Bisecting. Is current HEAD good or bad? Good Skip - Bisecting. Mark current commit as good or bad and checkout another one. + 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 + Holding Ctrl (on Windows/Linux) or ⌘ (on macOS) when clicking commit SHA will rebuild blame data based on that revision. BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout ${0}$... + Checkout ${0}$ as 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}$... @@ -89,6 +88,7 @@ {0} commit(s) ahead, {1} commit(s) behind {0} commit(s) behind Invalid + PUSH URL REMOTE STATUS TRACKING @@ -109,18 +109,26 @@ Submodule: URL: Checkout Branch - Checkout Commit - Commit: - Warning: By doing a commit checkout, HEAD will be detached Local Changes: 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 as Worktree + Local Branch: + New Branch Name: + Optional. Default is the destination folder name. + Remote Branch: + Tracking this remote branch. + Worktree Path: + Path for this worktree. Relative path is supported. 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): @@ -145,18 +153,19 @@ Branches & Tags Repository Custom Actions Revision Files - Checkout Commit - Cherry-Pick Commit - Cherry-Pick ... + 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... @@ -164,26 +173,23 @@ 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 + SELECT FILE TO VIEW CONTENT INFORMATION AUTHOR - CHILDREN COMMITTER Check refs that contains this commit COMMIT IS CONTAINED BY @@ -198,15 +204,18 @@ SHA Signer: Open in Browser - COL Enter commit message. Please use an empty-line to separate subject and description! SUBJECT Compare - Compare with HEAD + 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 @@ -217,8 +226,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 @@ -247,6 +256,7 @@ Minute(s) Conventional Commit Types Default Remote + Enable `--recursive` when auto-updating submodules Preferred Merge Mode ISSUE TRACKER Add Azure DevOps Rule @@ -304,6 +314,7 @@ Type of Change: Copy Copy All Text + Copy as Patch Copy Full Path Copy Path Create Branch... @@ -328,6 +339,7 @@ lightweight Hold Ctrl to start directly Cut + Dashboard Discard Do Nothing Stash & Reapply @@ -336,6 +348,7 @@ Submodule: Delete 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 @@ -357,7 +370,8 @@ Tag: Delete from remote repositories BINARY DIFF - File Mode Changed + VIEW NEW CONTENT + EMPTY FILE First Difference Ignore Whitespace Changes BLEND @@ -398,9 +412,6 @@ 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: @@ -416,6 +427,9 @@ Fetch without tags Remote: Fetch Remote Changes + BINARY FILE + HEX VIEWER + Go to address... Assume unchanged Custom Action Discard... @@ -433,14 +447,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: + Rebase before merging Squash during merge Hotfix: Hotfix Prefix: @@ -449,13 +474,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... @@ -487,21 +514,29 @@ Track files named '{0}' Track all *{0} files Select Commit - HISTORY + Advanced Options AUTHOR AUTHOR TIME COMMIT TIME - DATE TIME GRAPH & SUBJECT SHA + HIGHLIGHTS IN GRAPH + All + Current Branch Only + Current Branch & Selected Commits + Selected Commits Only + Selected Commits (First-parent 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 @@ -517,23 +552,17 @@ 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 'LOCAL 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 commit details panel + Toggle commit search panel + Switch to 'Local Changes' + Switch to 'Dashboard' + Switch to 'Stashes' Discard Stage Unstage @@ -551,6 +580,7 @@ Reverting commit Interactive Rebase Stash & reapply local changes + Bypass the pre-rebase hook On: Drag-drop to reorder commits Target Branch: @@ -559,6 +589,7 @@ Commands ERROR NOTICE + New Version Available Open Repositories Tabs Workspaces @@ -567,6 +598,10 @@ 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 @@ -598,6 +633,8 @@ Open Default Editor (System) Open Data Storage Directory + Cache + Config Open File Open in External Merge Tool Open Local Repository @@ -629,8 +666,12 @@ AI Additional Prompt (Use `-` to list your requirements) API Key + Extra Headers (one `Name: Value` per line) + Model + Fetch available model-ids automatically Name Entered value is the name to load API key from ENV + Reasoning Effort Level Server APPEARANCE Default Font @@ -658,13 +699,13 @@ 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 @@ -679,6 +720,7 @@ 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 @@ -710,6 +752,7 @@ Force push Local Branch: NEW + Bypass the pre-push hook Remote: Revision: Push Revision To Remote @@ -726,11 +769,17 @@ 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: Remote name + Reset push URL to this repository URL Repository URL: Remote git repository URL Copy URL @@ -741,6 +790,7 @@ Fetch Open In Browser Prune + Set Push URL... Confirm to Remove Worktree Enable `--force` Option Target: @@ -761,14 +811,16 @@ CONTINUE Custom Actions No Custom Actions - Dashboard Discard all changes Open in File Browser - Search Branches/Tags/Submodules + Filter 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 @@ -776,25 +828,24 @@ Commit Date Topologically LOCAL BRANCHES - More options... 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 + SEARCH COMMITS Author - Committer Content Message Path SHA Current Branch + SEARCH BY + Search Commits Decorated commits only First-parent only SHOW FLAGS @@ -807,12 +858,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 @@ -830,7 +880,6 @@ Revert Commit Commit: Commit revert changes - Reword Commit Message Running. Please wait... SAVE Save As... @@ -847,6 +896,10 @@ New Version Release Date: Software Update There are currently no updates available. + Set Push URL for Remote + Push URL: + Remote: + URL: Set Submodule's Branch Submodule: Current: @@ -858,11 +911,24 @@ Upstream: Copy SHA Go to - Squash HEAD into Parent - Fixup HEAD into Parent - Into: SSH Private Key: Private SSH key store path + Open SSH Key Helper + Do you really want to delete the following keys? + Fingerprint + Generate SSH Key + Confirm Passphrase: + Email: + Required. + Key Name: + Required. For example: id_github + Passphrase: + GENERATE + Type: + Use passphrase for private key + Path + Public Key + SSH Key Helper START Stash Include untracked files @@ -880,13 +946,19 @@ Save as Patch... Drop Stash Drop: - STASHES + Stashes CHANGES STASHES Statistics + COMMITS OVERVIEW MONTH WEEK + SourceGit will only get a maximum of 200,000 commits according to the target branch for analysis. + TOP AUTHORS + COMMITS + PERCENTAGE + Authors with less than 1% commits will be grouped under Others. AUTHORS: COMMITS: SUBMODULES @@ -916,6 +988,7 @@ OK TAGGER TIME + Checkout Tag... Compare 2 tags Compare with... Compare with HEAD @@ -928,10 +1001,17 @@ Delete selected {0} tags... Merge ${0}$ into ${1}$... Push ${0}$... + TERMINATE + Untrusted Repository + Do you want to run `git config --global --add safe.directory {0}` command to trust this path? + Git refused to access this repository: + Path: + {0} repositories were skipped because git does not trust them. Please open them manually and confirm whether to trust them. Update Submodules All submodules Initialize as needed Submodule: + Update nested submodules Update to submodule's remote tracking branch URL: Logs @@ -952,16 +1032,18 @@ Open Terminal Rescan Repositories in Default Clone Dir Search Repositories... - LOCAL CHANGES + Local Changes Git Ignore Ignore all *{0} files 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 Are you sure you want to clear all commit message history? This action cannot be undone. + Hide Sidebar COMMIT COMMIT & PUSH Template/History @@ -977,6 +1059,9 @@ FILE CONFLICTS ARE RESOLVED USE MINE USE THEIRS + DISCARD ALL + Show Sidebar + Filter Changes... INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 6bcce51247..e5a8c77e75 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -11,16 +11,11 @@ Agregar Archivo(s) Para Ignorar Patrón: Almacenar Archivo: - Agregar Worktree + Agregar Árbol de trabajo Ubicación: - Ruta para este worktree. Se admite ruta relativa. + Ruta para este árbol de trabajo. Se admite ruta relativa. Nombre de la Rama: Opcional. Por defecto es el nombre de la carpeta de destino. - Rama de Seguimiento: - Seguimiento de rama remota - Qué Checkout: - Crear Nueva Rama - Rama Existente Asistente OpenAI Modelo RE-GENERAR @@ -29,11 +24,12 @@ Ocultar SourceGit Ocultar Otros Mostrar Todo - Aplicar Parche Merge a 3 vías (3-Way) - Archivo del Parche: - Seleccionar archivo .patch para aplicar + Seleccionar archivo parche para aplicar Ignorar cambios de espacios en blanco + Fuente: + Archivo + Portapapeles Aplicar Parche Espacios en Blanco: Aplicar Stash @@ -51,22 +47,26 @@ NO HAY ARCHIVOS ASUMIDOS COMO SIN CAMBIOS Cargar Imagen... Refrescar - ¡ARCHIVO BINARIO NO SOPORTADO! Bisect Abortar Malo - Bisecting. ¿Es el HEAD actual bueno o malo? Bueno Saltar - Bisecting. Marcar el commit actual 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 + Mantener presionado Ctrl (en Windows/Linux) o ⌘ (en macOS) cuando se hace click en el SHA del commit reconstruirá lo datos del blame basados en esa revisión. ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! - Checkout ${0}$... + Checkout a ${0}$... + Checkout a ${0}$ como árbol de trabajo... 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}$... @@ -86,17 +86,18 @@ Rebase ${0}$ en ${1}$... Renombrar ${0}$... Resetear ${0}$ a ${1}$... - Cambiar a ${0}$ (worktree) + Cambiar a ${0}$ (árbol de trabajo) Establecer Rama de Seguimiento... {0} commit(s) adelante {0} commit(s) adelante, {1} commit(s) detrás {0} commit(s) detrás Inválido + PUSH URL REMOTO ESTADO SEGUIMIENTO URL - WORKTREE + ÁRBOL DE TRABAJO CANCELAR Resetear a Revisión Padre Resetear a Esta Revisión @@ -112,18 +113,26 @@ Submódulo: URL: Checkout Rama - Checkout Commit - Commit: - Advertencia: Al hacer un checkout de commit, tu Head se separará Cambios Locales: Rama: ¡Tu HEAD actual contiene commit(s) que no están conectados a ningunas ramas/etiquetas! ¿Quieres continuar? Los siguientes submódulos necesitan ser actualizados:{0} ¿Quieres actualizarlos? - Checkout & Fast-Forward + Checkout y Fast-Forward Fast-Forward a: + Checkout a Rama como Árbol de trabajo + Rama Local: + Nombre de la Nueva Rama: + Opcional. Por defecto es el nombre de la carpeta de destino. + Rama Remota: + Rastrear esta rama remota. + Ruta del Árbol de trabajo: + Ruta para este árbol de trabajo. Se puede usar una ruta relativa. Checkout a la Rama desde el Stash Nueva Rama: Stash: + Hacer checkout a Commit o Etiqueta + Destino: + Advertencia: Después de hacer esto, se deprenderá/separará de HEAD Cherry Pick Añadir fuente al mensaje de commit Commit(s): @@ -145,21 +154,22 @@ CERRAR Editor Ramas - Ramas & Etiquetas + 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 + Comparar con Árbol de trabajo Autor + Fecha/Hora del Autor Mensaje Committer + Fecha/Hora del Committer SHA Asunto Acción personalizada - Eliminar Commit Rebase interactivo Eliminar... Editar... @@ -167,32 +177,29 @@ 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 + SELECCIONAR ARCHIVO PARA VER SU CONTENIDO 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 @@ -201,11 +208,14 @@ 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 - Comparar con HEAD + 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: @@ -250,6 +260,7 @@ 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 @@ -293,8 +304,8 @@ Restaurar pestañas al iniciar CONTINUAR ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty)? - HACER STAGE A TODO & COMMIT - STAGE LO SELECCIONADO & 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. @@ -307,6 +318,7 @@ Tipo de Cambio: Copiar Copiar Todo el Texto + Copiar como Parche Copiar Ruta Completa Copiar Ruta Crear Rama... @@ -331,14 +343,16 @@ ligera Mantenga Ctrl para iniciar directamente Cortar + Tablero Descartar No Hacer Nada - Stash & Reaplicar + Stash y Volver a aplicar Desinicializar Submódulo Forzar desinicialización incluso si contiene cambios locales. Submódulo: Eliminar Rama 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 @@ -360,7 +374,8 @@ Etiqueta: Eliminar de los repositorios remotos DIFERENCIA BINARIA - Modo de Archivo Cambiado + VER CONTENIDO NUEVO + ARCHIVO VACÍO Primera Diferencia Ignorar Cambio de Espacios en Blanco MEZCLAR @@ -398,12 +413,9 @@ Cambios: Incluir archivos ignorados Incluir archivos modificados/eliminados - Incluir archivos no rastreados + 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: @@ -419,6 +431,9 @@ Fetch sin etiquetas Remoto: Fetch Cambios Remotos + ARCHIVO BINARIO + VISOR HEXADECIMAL + Ir a la dirección... Asumir como sin cambios Acción Personalizada Descartar... @@ -436,14 +451,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: + Hacer rebase antes de fusionar Squash durante el merge Hotfix: Prefijo de Hotfix: @@ -452,10 +478,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 @@ -490,18 +518,26 @@ Seguir archivos llamados '{0}' Seguir todos los archivos *{0} Seleccionar Commit - Historias + Opciones Avanzadas AUTOR HORA DEL AUTOR FECHA DE COMMIT - FECHA HORA - GRÁFICO & ASUNTO + GRÁFICO y ASUNTO SHA + DESTACADOS EN EL GRÁFICO + Todos + Solo la Rama Actual + Rama Actual y los Commits Seleccionados + Solo los Commits Seleccionados + Commits seleccionados (Solamente el primer padre) {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 @@ -520,23 +556,17 @@ 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 panel detallado del commit + Alternar panel de búsqueda del commit Cambiar a 'Cambios' - Cambiar a 'Historias' + Cambiar a 'Tablero' 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 @@ -553,15 +583,17 @@ 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: + Rama Destino: Copiar Enlace Abrir en el Navegador Comandos ERROR AVISO + Nueva Versión Disponible Abrir Repositorios Páginas Espacios de trabajo @@ -570,6 +602,10 @@ En: Opción de Merge: Rama Fuente: + 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 @@ -579,7 +615,7 @@ Conflicto Siguiente Conflicto Previo RESULTADO - GUARDAR & STAGE + GUARDAR y hacer STAGE SUYOS Conflictos de Merge ¿Descartar cambios sin guardar? @@ -601,6 +637,8 @@ Abrir Editor por defecto (Sistema) Abrir Directorio de Datos de la App + Cache + Configuración Abrir Archivo Abrir en Herramienta de Merge Abrir Repositorio Local @@ -632,8 +670,12 @@ OPEN AI Prompt adicional (Usa `-` para listar tus requerimientos) Clave API + Encabezados adicionales (un `Nombre: Valor` por línea) + Modelo + Traer automáticamente los model-ids disponibles Nombre El valor ingresado es el nombre de la clave API a cargar desde ENV + Nivel de Esfuerzo de Razonamiento Servidor APARIENCIA Fuente por defecto @@ -661,13 +703,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 @@ -682,6 +724,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 @@ -699,8 +742,8 @@ Shell/Terminal Podar Remoto Destino: - Podar Worktrees - Podar información de worktree en `$GIT_COMMON_DIR/worktrees` + Podar Árboles de trabajo + Podar información del árbol de trabajo en `$GIT_COMMON_DIR/worktrees` Pull Rama Remota: En: @@ -713,6 +756,7 @@ Forzar push Rama Local: NUEVO + Saltar el hook de pre-push Remoto: Revisión: Push Revisión al Remoto @@ -728,22 +772,30 @@ 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: Nombre remoto + Resetear la "push URL" a la URL de este repositorio URL del Repositorio: URL del repositorio git remoto Copiar URL Acción Personalizada Borrar... Editar... + Habilitar Auto-Fetch Fetch Abrir En Navegador Podar (Prune) - Confirmar para Eliminar Worktree + Establecer Push URL... + Confirmar para Eliminar Árbol de trabajo Utilizar Opción `--force` Destino: Renombrar Rama @@ -763,14 +815,16 @@ CONTINUAR Acciones Personalizadas No hay ninguna Acción Personalizada - Dashboard Descartar todos los cambios 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 @@ -778,11 +832,9 @@ Fecha de Commit Topológicamente RAMAS LOCALES - Más opciones... Navegar a HEAD Crear Rama LIMPIAR NOTIFICACIONES - Sólo resaltar la rama actual Abrir como Carpeta Abrir en {0} Abrir en Herramientas Externas @@ -791,12 +843,13 @@ RESOLVER Buscar Commit Autor - Committer Contenido Mensaje Ruta SHA Rama Actual + BUSCAR POR + Buscar Commits Sólo los commits decorados Mostrar sólo el primer padre MOSTRAR FLAGS @@ -814,11 +867,10 @@ Por Nombre Ordenar Abrir en Terminal - Usar tiempo relativo Ver Logs Visitar '{0}' en el Navegador - WORKTREES - AÑADIR WORKTREE + ÁRBOLES DE TRABAJO + AÑADIR ÁRBOL DE TRABAJO PRUNE URL del Repositorio Git Resetear Rama Actual a Revisión @@ -832,7 +884,6 @@ Revertir Commit Commit: Commit revertir cambios - Reescribir Mensaje de Commit Ejecutando. Por favor espera... GUARDAR Guardar Como... @@ -849,6 +900,10 @@ Fecha de Release de la Nueva Versión: Actualización de Software Actualmente no hay actualizaciones disponibles. + Establecer Push URL para el Remoto + Push URL: + Remoto: + URL: Establecer Rama del Submódulo Submódulo: Actual: @@ -860,14 +915,27 @@ Upstream: Copiar SHA Ir a - Squash HEAD en el Padre - Fixup HEAD en el Padre - En: Clave Privada SSH: Ruta de almacenamiento de la clave privada SSH + Abrir el Asistente para Claves SSH + ¿De verdad deseas borrar las siguientes claves? + Fingerprint + Generar Clave SSH + Confirmar Contraseña: + Email: + Requerido. + Nombre de la Clave: + Requerido. Por ejemplo: id_github + Contraseña: + GENERAR + Tipo: + Usar contraseña para la clave privada + Ruta + Clave Pública + Asistente para Claves SSH INICIAR Stash - Incluir archivos no rastreados + Incluir archivos sin rastrear Mensaje: Opcional. Información de este stash Modo: @@ -886,9 +954,15 @@ CAMBIOS STASHES Estadísticas + COMMITS GENERAL MES SEMANA + SourceGit solamente obtendrá un máximo de 200,000 commits de acuerdo a la rama destino para su análisis. + AUTORES DESTACADOS + COMMITS + PORCENTAJE + Autores con menos del 1% de commits serán agrupados bajo Otros. AUTORES: COMMITS: SUBMÓDULOS @@ -918,6 +992,7 @@ OK ETIQUETADOR HORA + Hacer checkout a Etiqueta... Comparar 2 etiquetas Comparar con... Comparar con HEAD @@ -928,12 +1003,19 @@ 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}$... + FINALIZAR + Repositorio No confiable + ¿Quieres correr el comando `git config --global --add safe.directory {0}` para confiar en está ruta? + Git rechazó el acceso a este repositorio: + Ruta: + {0} repositorios fueron omitidos porque git no confía en ellos. Ábrelos manualmente por favor y confirma si confías en ellos. Actualizar Submódulos Todos los submódulos Inicializar según sea necesario Submódulo: + Actualizar submódulos anidados Actualizar la rama de seguimiento remota del submódulo URL: Logs @@ -958,14 +1040,16 @@ 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. + Ocultar Barra lateral COMMIT - COMMIT & PUSH + COMMIT y PUSH Plantilla/Historias Activar evento de clic Commit (Editar) @@ -979,7 +1063,10 @@ LOS CONFLICTOS DE ARCHIVOS ESTÁN RESUELTOS USAR MÍOS USAR SUYOS - INCLUIR ARCHIVOS NO RASTREADOS + DESCARTAR TODO + Mostrar Barra lateral + Cambios de Filtro... + INCLUIR ARCHIVOS SIN RASTREAR NO HAY MENSAJES DE ENTRADA RECIENTES NO HAY PLANTILLAS DE COMMIT Sin verificar @@ -995,7 +1082,7 @@ Plantilla: ${0}$ ESPACIO DE TRABAJO: Configura Espacios de Trabajo... - WORKTREE + ÁRBOL DE TRABAJO RAMA Copiar Ruta HEAD diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index f2d0029647..e6b46a1e27 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 @@ -15,20 +16,16 @@ Chemin vers ce worktree. Relatif supporté. Nom de branche: Optionnel. Nom du dossier de destination par défaut. - Suivre la branche : - Suivi de la branche distante - Que récupérer : - 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 + Utiliser Masquer SourceGit + Masquer les autres Tout Afficher - Appliquer - Fichier de patch : - Selectionner le fichier .patch à appliquer + Fusion à 3 voies + Selectionner le fichier patch à appliquer Ignorer les changements d'espaces blancs Appliquer le patch Espaces blancs : @@ -47,24 +44,29 @@ PAS DE FICHIERS PRÉSUMÉS INCHANGÉS Charger l'Image... Rafraîchir - FICHIER BINAIRE NON SUPPORTÉ !!! Bisect Annuler Mauvais - Bisect en cours. Le HEAD actuel est-il bon ou mauvais ? Bon Passer - Bisect en cours. Marquer le commit actuel comme bon ou mauvais et en récupérer un autre. + 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 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}$ @@ -92,6 +94,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 @@ -100,14 +105,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 : 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 : @@ -119,6 +125,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 : @@ -126,6 +134,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 ... @@ -137,7 +149,6 @@ SHA Sujet Action personnalisée - Supprimer le Commit Rebase Interactif Supprimer... Modifier... @@ -145,26 +156,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 @@ -179,12 +186,18 @@ SHA Signataire : Ouvrir dans le navigateur + 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 @@ -195,8 +208,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 @@ -220,6 +233,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 @@ -256,8 +270,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 @@ -265,6 +282,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. @@ -330,7 +348,6 @@ Tag : Supprimer des dépôts distants DIFF BINAIRE - Mode de fichier changé Première différence Ignorer les changements d'espaces FUSIONNER @@ -350,6 +367,7 @@ SOUS-MODULE SUPPRIMÉ NOUVEAU + + {0} changements non commités Permuter Coloration syntaxique Retour à la ligne @@ -366,12 +384,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 : @@ -386,6 +404,7 @@ Remote : Récupérer les changements distants Présumer inchangé + Action personnalisée Rejeter... Rejeter {0} fichiers... Résoudre en utilisant ${0}$ @@ -454,16 +473,25 @@ Dépôt : Suivre les fichiers appelés '{0}' Suivre tous les fichiers *{0} - Historique + Sélectionner un commit AUTEUR HEURE DE L'AUTEUR HEURE DE COMMIT GRAPHE & SUJET SHA + 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 @@ -471,32 +499,31 @@ 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 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 @@ -508,6 +535,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 : @@ -524,6 +552,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: @@ -534,11 +578,17 @@ 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 Fermer l'onglet @@ -546,6 +596,8 @@ 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 @@ -560,7 +612,10 @@ Hier Préférences IA + Prompt supplémentaire (utilisez `-` pour lister vos exigences) Clé d'API + 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 @@ -577,6 +632,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 @@ -586,12 +645,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 @@ -606,6 +665,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 @@ -617,6 +677,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,6 +697,7 @@ Poussage forcé Branche locale : NOUVEAU + Contourner le hook de pre-push Dépôt distant : Révision : Pousser la Révision vers le Dépôt Distant @@ -651,6 +714,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 @@ -662,6 +726,7 @@ Action personnalisée Supprimer... Editer... + Activer l'auto-fetch Fetch Ouvrir dans le navigateur Elaguer @@ -685,7 +750,6 @@ CONTINUER Actions personnalisées Pas d'actions personnalisées - Tableau de bord Rejeter tous les changements Ouvrir dans l'explorateur de fichiers Rechercher Branches/Tags/Submodules @@ -700,18 +764,17 @@ Date du commit Topologiquement BRANCHES LOCALES - Plus d'options... Naviguer vers le HEAD Créer une branche EFFACER LES NOTIFICATIONS - Surligner uniquement la branche actuelle + Ouvrir 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 @@ -734,7 +797,6 @@ Par nom Trier Ouvrir dans un terminal - Utiliser l'heure relative Voir les Logs Visiter '{0}' dans le navigateur WORKTREES @@ -752,7 +814,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... @@ -762,9 +823,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 @@ -790,6 +853,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... @@ -826,9 +891,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 @@ -836,7 +906,6 @@ 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 @@ -881,6 +950,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 + 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 @@ -902,9 +973,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 0000000000..98426b3023 --- /dev/null +++ b/src/Resources/Locales/he_IL.axaml @@ -0,0 +1,985 @@ + + + + + + אודות + אודות SourceGit + תאריך שחרור: {0} + הערות שחרור + לקוח Git גרפי, חופשי ובקוד פתוח + הוסף קבצים ל־Ignore + תבנית: + קובץ אחסון: + הוסף Worktree + מיקום: + נתיב ל־worktree זה. נתיב יחסי נתמך. + שם 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 מרוחק!!! + למחוק גם את ה־branch המרוחק ${0}$ + מחק מספר 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, מתחיל מיידית + עבור לצאצא של ה־commit שנבחר + עבור להורה של ה־commit שנבחר + פתח command palette + Pull, מתחיל מיידית + Push, מתחיל מיידית + טען את המאגר מחדש בכוח + עבור ל־'LOCAL CHANGES' + עבור ל־'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 מקומי: + חדש + דלג על pre-push hook + 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` עבור המאגר. + נקה הכול + נקה + הגדר מאגר זה + המשך + פעולות מותאמות אישית + אין פעולות מותאמות אישית + השלך כל השינויים + פתח בסייר הקבצים + חיפוש 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 621abaa9c3..a50be71a66 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: @@ -14,21 +16,20 @@ Jalur untuk worktree ini. Jalur relatif didukung. Nama Branch: Opsional. Standar adalah nama folder tujuan. - Track Branch: - Track remote branch - Yang Akan Di-Checkout: - Buat Branch Baru - Branch Yang Ada Asisten AI Model BUAT ULANG Gunakan AI untuk membuat pesan commit + Gunakan Sembunyikan SourceGit + Sembunyikan Lainnya Tampilkan Semua - Patch - Berkas Patch: - Pilih berkas .patch untuk diterapkan + Merge 3 Arah + Pilih berkas patch untuk diterapkan Abaikan perubahan whitespace + Sumber: + Berkas + Papan Klip Terapkan Patch Whitespace: Terapkan Stash @@ -46,21 +47,31 @@ TIDAK ADA BERKAS YANG DIASUMSIKAN TIDAK BERUBAH Muat Gambar... Segarkan - BERKAS BINARY TIDAK DIDUKUNG!!! Bisect Batalkan Buruk - Bisect berjalan. Apakah HEAD saat ini baik atau buruk? Baik Lewati - Bisect berjalan. Tandai commit saat ini sebagai baik atau buruk dan checkout yang lain. + 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 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}$ @@ -88,6 +99,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 @@ -96,14 +110,18 @@ Submodule: URL: Checkout Branch - Checkout Commit - Commit: - Peringatan: Dengan melakukan checkout commit, Head akan terlepas Perubahan Lokal: 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: @@ -115,6 +133,8 @@ Clone Repositori Remote Parameter Tambahan: Argumen tambahan untuk clone repositori. Opsional. + Markah: + Grup: Nama Lokal: Nama repositori. Opsional. Folder Parent: @@ -122,14 +142,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 @@ -140,26 +166,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 @@ -174,16 +196,30 @@ SHA Penanda Tangan: Buka di Browser + 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 @@ -207,9 +243,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 @@ -242,7 +281,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 @@ -250,6 +293,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. @@ -262,6 +306,7 @@ Jenis Perubahan: Salin Salin Semua Teks + Salin sebagai Patch Salin Jalur Lengkap Salin Jalur Buat Branch... @@ -287,12 +332,14 @@ 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 Branch: + Paksa hapus meskipun berisi commit yang belum di-merge Anda akan menghapus remote branch!!! Juga hapus remote branch ${0}$ Hapus Beberapa Branch @@ -314,7 +361,7 @@ Tag: Hapus dari repositori remote DIFF BINARY - Mode Berkas Berubah + BERKAS KOSONG Perbedaan Pertama Abaikan Perubahan Whitespace BLEND @@ -334,6 +381,7 @@ PERUBAHAN SUBMODULE DIHAPUS BARU + + {0} Perubahan Belum Di-commit Tukar Syntax Highlighting Word Wrap Baris @@ -350,9 +398,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: @@ -367,6 +418,7 @@ Remote: Fetch Perubahan dari Remote Asumsikan tidak berubah + Aksi Kustom Buang... Buang {0} berkas... Selesaikan Menggunakan ${0}$ @@ -382,14 +434,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: + Rebase sebelum merge Squash saat merge Hotfix: Prefix Hotfix: @@ -398,10 +461,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 @@ -421,6 +486,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 @@ -433,16 +500,25 @@ Remote: Track berkas bernama '{0}' Track semua berkas *{0} - RIWAYAT + Pilih Commit AUTHOR WAKTU AUTHOR WAKTU COMMIT GRAFIK & SUBJEK SHA + 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 @@ -450,31 +526,31 @@ 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) - Mode pencarian commit + Ke turunan commit yang dipilih + Ke induk commit yang dipilih + Buka palet perintah Pull, langsung dimulai Push, langsung dimulai Paksa muat ulang repositori ini Pindah ke 'Changes' - Pindah ke 'History' Pindah ke 'Stashes' - TEXT EDITOR - Tutup panel pencarian - Cari kecocokan berikutnya - Cari kecocokan sebelumnya - Buka dengan diff/merge tool eksternal - Buka panel pencarian Buang Stage Unstage Inisialisasi Repositori + Apakah Anda ingin menjalankan perintah `git init` di jalur ini? + Gagal membuka repositori. Alasan: Jalur: Cherry-Pick sedang berjalan. Memproses commit @@ -486,13 +562,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 @@ -500,6 +580,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: @@ -510,9 +610,17 @@ 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 Tutup Tab @@ -520,6 +628,8 @@ Tutup Tab di Kanan Salin Jalur Repositori Sunting + Pindahkan ke Workspace + Segarkan Repositori Tempel {0} hari lalu @@ -534,7 +644,11 @@ Kemarin Preferensi AI + Prompt Tambahan (Gunakan `-` untuk mencantumkan kebutuhan Anda) API Key + Header Tambahan (satu `Nama: Nilai` per baris) + Model + Ambil model-id yang tersedia secara otomatis Nama Nilai yang dimasukkan adalah nama untuk memuat API key dari ENV Server @@ -551,6 +665,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 @@ -560,12 +678,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 @@ -580,6 +699,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 @@ -591,6 +711,8 @@ Kunci penandatanganan gpg pengguna INTEGRASI SHELL/TERMINAL + Argumen + Gunakan '.' untuk menunjukkan direktori kerja Jalur Shell/Terminal Prune Remote @@ -609,6 +731,7 @@ Paksa push Branch Lokal: BARU + Lewati hook pre-push Remote: Revisi: Push Revisi ke Remote @@ -620,10 +743,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: @@ -634,6 +764,7 @@ Aksi Kustom Hapus... Sunting... + Aktifkan Fetch Otomatis Fetch Buka di Browser Prune @@ -657,14 +788,16 @@ LANJUTKAN Aksi Kustom Tidak Ada Aksi Kustom - Dashboard Buang semua perubahan 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 @@ -672,18 +805,17 @@ Tanggal Commit Topologis BRANCH LOKAL - Opsi lainnya... 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 @@ -706,7 +838,6 @@ Berdasarkan Nama Urut Buka di Terminal - Gunakan waktu relatif Lihat Log Kunjungi '{0}' di Browser WORKTREE @@ -724,7 +855,6 @@ Revert Commit Commit: Commit perubahan revert - Reword Pesan Commit Sedang berjalan. Harap tunggu... SIMPAN Simpan Sebagai... @@ -734,9 +864,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 @@ -762,6 +894,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... @@ -798,9 +932,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 @@ -814,6 +954,7 @@ Semua submodule Inisialisasi sesuai kebutuhan Submodule: + Perbarui submodule bertingkat Perbarui ke remote tracking branch submodule URL: Log @@ -840,6 +981,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 @@ -853,6 +995,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 + MERGE + Merge dengan Alat Eksternal BUKA SEMUA KONFLIK DI MERGETOOL EKSTERNAL KONFLIK BERKAS DISELESAIKAN GUNAKAN MILIK SAYA @@ -874,9 +1018,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 629f6da4d0..3ce0bae2c9 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -16,20 +16,13 @@ Percorso per questo worktree. Supportato il percorso relativo. Nome Branch: Facoltativo. Predefinito è il nome della cartella di destinazione. - Traccia Branch: - Traccia branch remoto - Di cosa fare il checkout: - Crea nuovo branch - Branch esistente Assistente AI Modello RIGENERA Usa AI per generare il messaggio di commit Nascondi SourceGit Mostra Tutto - Applica - File Patch: - Seleziona file .patch da applicare + Seleziona file patch da applicare Ignora modifiche agli spazi Applica Patch Spazi: @@ -48,14 +41,13 @@ NESSUN FILE ASSUNTO COME INVARIATO Carico l'Immagine... Aggiorna - FILE BINARIO NON SUPPORTATO!!! Biseca Annulla Cattiva - Bisecando. La HEAD corrente è buona o cattiva? Buona Salta - Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. + 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 @@ -108,9 +100,6 @@ Sottomodulo: URL: Checkout Branch - Checkout Commit - Commit: - Avviso: Effettuando un checkout del commit, la tua HEAD sarà separata Modifiche Locali: Branch: Il tuo HEAD attuale contiene commit non connessi ad alcun branch/tag! Sicuro di voler continuare? @@ -146,7 +135,6 @@ SHA Oggetto Azione Personalizzata - Scarta Commit Rebase Interattivo Scarta... Modifica... @@ -154,16 +142,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... @@ -173,7 +157,6 @@ Sottomodulo INFORMAZIONI AUTORE - FIGLI CHI HA COMMITTATO Controlla i riferimenti che contengono questo commit IL COMMIT È CONTENUTO DA @@ -191,7 +174,6 @@ Inserisci il messaggio di commit. Usa una riga vuota per separare oggetto e descrizione! OGGETTO Confronto - Confronta con HEAD Configura Repository TEMPLATE DI COMMIT Parametri integrati: @@ -207,7 +189,7 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle 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 @@ -342,7 +324,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Tag: Elimina dai repository remoti DIFF BINARIO - Modalità File Modificata Prima differenza Ignora Modifiche agli Spazi FUSIONE @@ -381,9 +362,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Includi file non tracciati Un totale di {0} modifiche saranno scartate Questa azione non può essere annullata!!! - Scarta Commit - Commit: - Nuovo HEAD: Modifica Descrizione del Branch Destinazione: Segnalibro: @@ -469,7 +447,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Remoto: Traccia file con nome '{0}' Traccia tutti i file *{0} - STORICO AUTORE ORA AUTORE ORA COMMIT @@ -495,21 +472,12 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle 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 Forza l'aggiornamento di questo repository 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 @@ -627,10 +595,8 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle 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 pagina `MODIFICHE LOCALI` per impostazione predefinita Mostra scheda `MODIFICHE` nei dettagli del commit per impostazione predefinita - Mostra i figli nei dettagli del commit Mostra i tag nel grafico dei commit Lunghezza Guida Oggetto Genera avatar predefinito stile GitHub @@ -678,6 +644,7 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Forza l'invio Branch Locale: NUOVO + Ignora l'hook di pre-push Remoto: Revisione: Invia Revisione Al Remoto @@ -728,7 +695,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle CONTINUA Azioni Personalizzate Nessuna Azione Personalizzata - Dashboard Scarta tutte le modifiche Apri nell'Esplora File Cerca Branch/Tag/Sottomodulo @@ -743,11 +709,9 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Per data del commit Topologicamente BRANCH LOCALI - Altre opzioni... Vai a HEAD Crea Branch CANCELLA LE NOTIFICHE - Evidenzia solo il branch corrente Apri come Cartella Apri in {0} Apri in Strumenti Esterni @@ -756,7 +720,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle RISOLVI Cerca Commit Autore - Committer Contenuto Messaggio Percorso @@ -779,7 +742,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Per nome Ordina Apri nel Terminale - Usa tempo relativo Visualizza i Log Visita '{0}' nel Browser WORKTREE @@ -797,7 +759,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Ripristina Commit Commit: Commit delle modifiche di ripristino - Modifica Messaggio di Commit In esecuzione. Attendere... SALVA Salva come... @@ -823,9 +784,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Upstream: Copia SHA Vai a - Squash HEAD nel Genitore - Fixup HEAD nel Genitore - In: Chiave Privata SSH: Percorso per la chiave SSH privata AVVIA @@ -887,7 +845,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Azione Personalizzata Elimina ${0}$... Elimina i {0} tag selezionati... - Unisci ${0}$ in ${1}$... Invia ${0}$... Aggiorna Sottomoduli Tutti i sottomoduli diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 26c1701bfa..ac9637eea9 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -16,21 +16,20 @@ この作業ツリーへのパス (相対パスも使用できます) ブランチの名前: 省略可能 - 既定では宛先のフォルダーと同じ名前が使用されます - 追跡するブランチ: - 追跡するリモートブランチを設定 - チェックアウトする内容: - 新しいブランチを作成 - 既存のブランチ AI アシスタント モデル 再生成 AI を使用してコミットメッセージを生成 + 使用 SourceGit を隠す + 他を隠す すべて表示 - パッチ - パッチファイル: - 適用する .patch ファイルを選択 + 3 分割マージ + 適用するパッチファイルを選択 空白文字の変更を無視 + 適用元: + ファイル + クリップボード パッチを適用 空白文字: スタッシュを適用 @@ -48,38 +47,41 @@ 未変更と見なしたファイルはありません 画像を選択... 再読み込み - バイナリファイルはサポートされていません!!! 問題の発生源を特定 中止 問題あり - 問題の発生源となったコミットを特定しています。現在の HEAD に問題はありませんか? 問題なし スキップ - 問題の発生源となったコミットを特定しています。現在のコミットに問題があるかどうかを調査したあと、別のコミットをチェックアウトしてみてください。 + 問題の発生源となったコミットを特定しています。別のコミットをチェックアウトし、そのコミットに問題があるかどうかを調査してください。 + 問題の発生源となったコミットを特定しています。問題があるコミットをチェックアウト・選択してください。 + 問題の発生源となったコミットを特定しています。現在のコミットに問題があるかどうかを調査したあと、別のコミットをチェックアウトしてみてください。 + 問題の発生源となったコミットを特定しています。現在の HEAD に問題はありませんか? 著者の履歴 前のリビジョンの著者の履歴 空白文字の変更を無視 + Ctrl (Windows/Linux) または ⌘ (macOS) キーを押しながらコミットの SHA をクリックすると、そのリビジョンを基点に著者の履歴データを再構築します。 このファイルに著者の履歴は表示できません!!! ${0}$ をチェックアウト... 選択した 2 つのブランチを比較 比較対象を選択... HEAD と比較 + ${0}$ と比較 ブランチ名をコピー プルリクエストを作成... 上流の ${0}$ にプルリクエストを作成... カスタムアクション ${0}$ を削除... - 選択した {0} 個のブランチを削除 + 選択した {0} 個のブランチを削除... ${0}$ の説明を編集... ${0}$ まで早送り - ${0}$ から ${1}$ にフェッチ... - Git フロー - ${0}$ を完了 + ${0}$ から ${1}$ にフェッチ + Git フロー - ${0}$ を完了... ${0}$ を ${1}$ で対話式リベース ${0}$ を ${1}$ にマージ... - 現在のブランチに、選択した {0} 個のブランチをマージ - ${0}$ からプル + 現在のブランチに、選択した {0} 個のブランチをマージ... + ${0}$ からプル... ${0}$ から ${1}$ にプル... - ${0}$ をプッシュ + ${0}$ をプッシュ... ${0}$ を ${1}$ でリベース... ${0}$ の名前を変更... ${0}$ を ${1}$ にリセット... @@ -89,6 +91,7 @@ ローカルに {0} コミット済み、リモートに {1} コミットあり リモートに {0} コミットあり 無効 + プッシュ先の URL リモート 状態 追跡対象 @@ -109,15 +112,18 @@ サブモジュール: URL: ブランチをチェックアウト - コミットをチェックアウト - コミット: - 警告: コミットをチェックアウトすると、HEAD が切断されます ローカルの変更: ブランチ: 現在の HEAD には、どのブランチやタグにも繋がっていないコミットが含まれています!それでも続行しますか? これらのサブモジュールを更新する必要があります:{0}更新しますか? チェックアウト & 早送り 早送り先: + スタッシュからブランチをチェックアウト + 新しいブランチ: + スタッシュ: + コミットまたはタグをチェックアウト + 対象: + 警告: これを行うと、HEAD が切断されます コミットを取り込む 取り込み元をコミットメッセージに明記 コミット: @@ -129,6 +135,8 @@ リモートリポジトリをクローン 追加の引数: リポジトリをクローンする際の追加の引数 (省略可能) + ブックマーク: + グループ: ローカル名: リポジトリの名前 (省略可能) 親フォルダー: @@ -136,18 +144,23 @@ リポジトリの URL: 閉じる エディター - コミットをチェックアウト - コミットを取り込む + ブランチ + ブランチ & タグ + リポジトリのカスタムアクション + リビジョンのファイル + コミットをチェックアウト... + コミットを取り込む... 取り込む... HEAD と比較 作業ツリーと比較 著者 + 著者の日時 メッセージ コミッター + コミット日時 SHA タイトル カスタムアクション - コミットを削除 対話式リベース 削除... 編集... @@ -155,26 +168,22 @@ ${0}$ を ${1}$ で対話式リベース 書き直す... 親コミットに記録付きで統合... - ${0}$ にマージ マージ... - ${0}$ を ${1}$ にプッシュ - ${0}$ を ${1}$ でリベース - ${0}$ を ${1}$ にリセット - コミットを取り消す - 書き直す + ${0}$ を ${1}$ にプッシュ... + ${0}$ を ${1}$ でリベース... + ${0}$ を ${1}$ にリセット... + コミットを取り消す... パッチとして保存... - 親コミットに記録付きで統合 - 親コミットに統合 変更 個の変更されたファイル 変更を検索... + 詳細を折りたたむ ファイル LFS ファイル ファイルを検索... サブモジュール コミットの情報 著者 - 子コミット コミッター このコミットが含まれる参照を確認 これらの参照にコミットが含まれています @@ -189,11 +198,14 @@ SHA 署名者: ブラウザーで開く - 列 コミットメッセージを入力してください。タイトルと説明は、空行で分けて記述してください! タイトル 比較 - HEAD と比較 + 変更 + コミット + 左側のみに含まれるコミット + 右側のみに含まれるコミット + ヒント: もう片側が HEAD を指しているのであれば、(コンテキストメニューから) コミットを選択して取り込むことができます。 リポジトリの設定 コミットテンプレート 組み込みのパラメーター: @@ -238,6 +250,7 @@ 分ごとに自動フェッチ コンベンショナルコミットの種類定義 既定のリモート + サブモジュールを自動更新する際は再帰的に実行 優先するマージ方式 イシュートラッカー Azure DevOps のルールを追加 @@ -270,8 +283,11 @@ ラベル: 選択肢: 選択肢は '|' で区切って記述します + 文字列の整形ツール: + 省略可能 - 最終的な出力文字列を整形するために使用されます (入力がなければ何もしません)。入力された値は `${VALUE}` で参照してください。 ここでも組み込みの変数 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, ${TAG} がそのまま利用できます 種類: + リモート名付きの名前を使用: ワークスペース 色 名前 @@ -279,7 +295,8 @@ 続行 コミットの内容が何もありません!それでも続行しますか (--allow-empty)? すべてステージに上げてからコミット - コミットの内容が何もありません!コミットする前に、すべての変更をステージに上げますか?それともこのまま続行しますか (--allow-empty)? + 選択した変更をステージに上げてからコミット + コミットの内容が何もありません!コミットする前に、変更をステージに上げますか?それともこのまま続行しますか (--allow-empty)? 再起動が必要です この変更は、アプリを再起動したあとに反映されます。 コンベンショナルコミットの生成補助ツール @@ -291,6 +308,7 @@ 変更の種類: コピー すべてのテキストをコピー + パッチとしてコピー 絶対パスをコピー パスをコピー ブランチを作成... @@ -309,19 +327,22 @@ タグ名: 推奨される形式: v1.0.0-alpha タグの作成後、すべてのリモートにプッシュ - 新しいタグを作成 + タグを作成 種類: 注釈付き 軽量 Ctrl キーを押しながらで直接実行できます 切り取り + ダッシュボード 破棄 + 何もしない スタッシュして再適用 サブモジュールの初期化を解除 ローカルの変更の有無に関わらず、強制的に解除 サブモジュール: ブランチを削除 ブランチ: + 未マージのコミットがあっても強制的に削除 リモートブランチを削除しようとしています!!! リモートブランチの ${0}$ も削除 複数のブランチを削除 @@ -343,7 +364,8 @@ タグ: リモートリポジトリからも削除 バイナリの差分 - ファイルモードが変更されました + 新しい内容を表示 + 空のファイル 最初の差分 空白文字の変更を無視 ブレンド @@ -363,6 +385,7 @@ サブモジュール 削除 新規 + さらに {0} 件のコミットされていない変更があります 入れ替え 構文を強調表示 行を折り返す @@ -379,12 +402,10 @@ 作業コピーに対するローカルのすべての変更 変更: 無視されたファイルを含める + 変更/削除されたファイルを含める 未追跡のファイルを含める {0} 件の変更が破棄されます この操作は元に戻せません!!! - コミットを削除 - コミット: - 新しい HEAD: ブランチの説明を編集 対象: ブックマーク: @@ -400,6 +421,9 @@ タグなしでフェッチ リモート: リモートの変更をフェッチ + バイナリファイル + バイナリビューアー + アドレスに移動... 未変更と見なす カスタムアクション 破棄... @@ -417,14 +441,25 @@ ファイルの履歴 変更 ファイルの内容 + ファイルモードの変更: + 削除されたファイルのモード: + ディレクトリ + 実行可能 + 新しいファイルのモード: + 通常 + サブモジュール + シンボリックリンク + 不明 Git フロー 開発ブランチ: 新機能の実装: 新機能の実装用のプレフィックス: - フロー - 新機能の実装を完了 - フロー - 緊急のバグ修正を完了 - フロー - リリース作業を完了 + ${0}$ を完了 + 新機能の実装を完了 + 緊急のバグ修正を完了 + リリース作業を完了 対象: + マージする前にリベース スカッシュしてマージ 緊急のバグ修正: 緊急のバグ修正用のプレフィックス: @@ -433,13 +468,15 @@ 本番ブランチ: リリース作業: リリース作業用のプレフィックス: + 開始地点: 新機能の実装を開始... - フロー - 新機能の実装を開始 + 新機能の実装を開始 緊急のバグ修正を開始... - フロー - 緊急のバグ修正を開始 + 緊急のバグ修正を開始 + 名前: 名前を入力 リリース作業を開始... - フロー - リリース作業を開始 + リリース作業を開始 バージョンタグのプレフィックス: Git LFS 追跡パターンを追加... @@ -471,25 +508,34 @@ '{0}' という名前のファイルを追跡 *{0} ファイルをすべて追跡 コミットを選択 - 履歴 + 詳細オプション 著者 著者の日時 コミット日時 - 日時 グラフ & コミットのタイトル SHA + グラフでの強調表示 + 全範囲 + 現在のブランチのみ + 現在のブランチ & 選択したコミット + 選択したコミットのみ + 選択したコミットかつ最初の親コミットのみ {0} コミットを選択しました 表示する列 'Ctrl' または 'Shift' キーを押しながらで、複数のコミットを選択できます。 ⌘ または ⇧ キーを押しながらで、複数のコミットを選択できます。 ヒント: + 別のウィンドウで開く + コミットの詳細 + リビジョンを比較 キーボードショートカットを確認 総合 - 新しいリポジトリをクローン + リモートリポジトリをクローン 現在のタブを閉じる 次のタブに移動 前のタブに移動 新しいタブを作成 + ローカルリポジトリを開く 設定ダイアログを開く ワークスペースのドロップダウンメニューを表示 アクティブなタブを切り替え @@ -498,28 +544,25 @@ ステージに上げた変更をコミット ステージに上げた変更をコミットしてプッシュ すべての変更をステージに上げてコミット + 新しいブランチを作成 フェッチ (直接実行) - ダッシュボードモード (既定) 選択したコミットの子コミットに移動 選択したコミットの親コミットに移動 コマンドパレットを開く - コミット検索モード プル (直接実行) プッシュ (直接実行) 現在のリポジトリを強制的に再読み込み - '変更' に切り替え - '履歴' に切り替え + コミットの詳細パネルを広げる/折りたたむ + コミットの検索パネルを切り替え + 'ローカルの変更' に切り替え + 'ダッシュボード' に切り替え 'スタッシュ' に切り替え - テキストエディター - 検索パネルを閉じる - 次の一致を検索 - 前の一致を検索 - 外部の比較/マージツールで開く - 検索パネルを開く 破棄 ステージに上げる ステージから降ろす リポジトリを初期化 + このパスで `git init` コマンドを実行しますか? + リポジトリを開けませんでした。理由: パス: 取り込み作業が進行中です。 取り込み対象のコミット: @@ -531,6 +574,7 @@ 取り消し対象のコミット: 対話式リベース ローカルの変更をスタッシュして再適用 + リベース前のフックを実行しない リベース地点: ドラッグ & ドロップでコミットを並べ替えられます 対象のブランチ: @@ -539,6 +583,7 @@ コマンド エラー 通知 + 新しいバージョンが利用できます リポジトリを開く タブ ワークスペース @@ -547,6 +592,10 @@ マージ先: マージオプション: マージ元: + マージができるかを確認しています... + マージは衝突せずに行えます + マージ時に何らかのエラーが発生します + マージに衝突が発生します 自分の変更 → 相手の変更の順で適用 相手の変更 → 自分の変更の順で適用 両方を使用 @@ -578,8 +627,14 @@ 開く 既定のエディター (システム) データの保管ディレクトリを開く + キャッシュ + 設定 ファイルを開く 外部のマージツールで開く + ローカルリポジトリを開く + ブックマーク: + グループ: + フォルダー: 省略可能 新しいタブを作成 タブを閉じる @@ -603,9 +658,14 @@ 昨日 設定 AI + 追加のプロンプト (`-` で必要事項を羅列してください) API キー + 追加のヘッダー (`名前: 値` を一行ごとに指定してください) + モデル + 利用できるモデル ID を自動的に取得 名前 この値を環境変数の名前とし、そこから API キーを読み込む + 思考努力レベル サーバー 外観 既定のフォント @@ -628,17 +688,18 @@ 差分/マージツールへのパスを入力 ツール 総合 - 起動時にアップデートを確認 + 起動時に更新を確認 日付の書式 変更ツリーのフォルダー階層をまとめる 言語 コミット履歴 - グラフにコミット日時ではなく著者の日時を表示 初めから `ローカルの変更` ページを表示 初めからコミットの詳細の `変更` タブを表示 - コミットの詳細に子コミットを表示 + コミットグラフを相対時間で表記 コミットグラフにタグを表示 適切とするコミットタイトルの長さ + 24 時間 + コミットグラフのブランチ名をまとめる GitHub のような既定のアバターを生成 Git 自動 CRLF 変換を有効化 @@ -653,6 +714,7 @@ git-credential-manager ではなく git-credential-libsecret を使用 ユーザー名 Git ユーザーの名前 (グローバル設定) + ブランチのチェックアウトやマージ時に、既定で変更をスタッシュして再適用 Git バージョン GPG 署名 コミットを GPG で署名 @@ -684,6 +746,7 @@ 強制的にプッシュ ローカルブランチ: 新規 + プッシュ前のフックを実行しない リモート: リビジョン: リビジョンをリモートにプッシュ @@ -700,20 +763,28 @@ 終了 現在のブランチをリベース ローカルの変更をスタッシュして再適用 + リベース前のフックを実行しない リベース地点: + リベースができるかを確認しています... + リベースは衝突せずに行えます + リベース時に何らかのエラーが発生します + リベースに衝突が発生します リモートを追加 リモートを編集 名前: リモート名 + プッシュ先の URL を、このリポジトリの URL にリセット リポジトリの URL: リモートの Git リポジトリの URL URL をコピー カスタムアクション 削除... 編集... + 自動フェッチを有効化 フェッチ ブラウザーで開く 掃除 + プッシュ先の URL を設定... 作業ツリーの削除を確認 `--force` オプションを有効化 対象: @@ -734,14 +805,16 @@ 続行 カスタムアクション カスタムアクションがありません - ダッシュボード すべての変更を破棄 ファイルブラウザーで開く - ブランチ/タグ/サブモジュールを検索 + ブランチ/タグ/サブモジュールを絞り込み グラフでの可視性 + 絞り込み条件を折りたたむ 解除 コミットグラフから隠す + 絞り込み条件を広げる コミットグラフで絞り込む + 個の条件で絞り込んでいます レイアウト 水平 垂直 @@ -749,11 +822,9 @@ コミット日時 トポロジカル ローカルブランチ - その他のオプション... HEAD に移動 ブランチを作成 通知を消去 - 現在のブランチのみを強調表示 フォルダーとして開く {0} で開く 外部ツールで開く @@ -762,12 +833,13 @@ 解決 コミットを検索 著者 - コミッター ファイルの内容 メッセージ パス SHA 現在のブランチ + 検索対象 + コミットを検索 参照付きのコミットのみ 最初の親コミットのみ 表示フラグ @@ -780,12 +852,11 @@ サブモジュールを追加 サブモジュールを更新 タグ - 新しいタグ + タグを作成 作成者の日時順 名前順 並べ替え 端末で開く - 相対時間を使用 ログを表示 ブラウザーで '{0}' を訪問 作業ツリー @@ -803,7 +874,6 @@ コミットを取り消す コミット: 取り消しの変更をコミット - コミットメッセージを書き直す 実行しています。お待ちください... 保存 名前を付けて保存... @@ -820,6 +890,10 @@ 新しいバージョンのリリース日: ソフトウェアの更新 利用できる更新はありません。 + リモートにプッシュ先の URL を設定 + プッシュ先の URL: + リモート: + URL: サブモジュールのブランチを設定 サブモジュール: 現在: @@ -831,22 +905,36 @@ 上流のブランチ: SHA をコピー このコミットに移動 - 親コミットに記録付きで統合 - 親コミットに統合 - 統合先: SSH の秘密鍵: SSH の秘密鍵が保管されているパス + SSH の鍵ツールを開く + これらの鍵を削除しますか? + フィンガープリント + SSH 鍵を生成 + パスフレーズを確認: + メールアドレス: + 必須 + 鍵の名前: + 必須 - 例: id_github + パスフレーズ: + 生成 + 種類: + 秘密鍵にパスフレーズを使用 + パス + 公開鍵 + SSH の鍵ツール 開始 スタッシュ 未追跡のファイルを含める メッセージ: 省略可能 - このスタッシュのメッセージ 方式: - ステージに上げた変更のみ + ステージに上げたファイルのみ 選択したファイルの、ステージに上がっている変更とそうではない変更の両方がスタッシュされます!!! ローカルの変更をスタッシュ 適用 変更を適用 + 新しいブランチをチェックアウト メッセージをコピー 削除 パッチとして保存... @@ -859,6 +947,7 @@ 概要 月間 週間 + 対象のブランチの 200,000 コミットまでのみを分析して表示しています。 著者: コミット: サブモジュール @@ -867,7 +956,7 @@ ブランチ 相対パス 初期化を解除 - 入れ子になったサブモジュールを取得 + 入れ子になったサブモジュールも取得 履歴 移動 リポジトリを開く @@ -883,9 +972,12 @@ 未マージ 更新 URL + サブモジュールの変更の詳細 + 詳細を開く OK タグの作成者 日時 + タグをチェックアウト... 2 つのタグを比較 比較対象を選択... HEAD と比較 @@ -898,10 +990,12 @@ 選択した {0} 個のタグを削除... ${0}$ を ${1}$ にマージ... ${0}$ をプッシュ... + 中止 サブモジュールを更新 すべてのサブモジュール 必要に応じて初期化 サブモジュール: + 入れ子になったサブモジュールも更新 サブモジュールのリモート追跡ブランチに更新 URL: ログ @@ -924,10 +1018,11 @@ リポジトリを検索... ローカルの変更 無視 - すべての *{0} ファイルを無視 - 同一フォルダーの *{0} ファイルを無視 + *{0} ファイルをすべて無視 + 同じフォルダーの *{0} ファイルを無視 このフォルダーで未追跡のファイルを無視 このファイルのみを無視 + 同じフォルダーで未追跡のファイルをすべて無視 最後のコミットをやり直す このファイルをステージに上げられるようになりました。 履歴を消去 @@ -947,6 +1042,8 @@ ファイルの衝突が解決されました 自分の変更を使用 相手の変更を使用 + すべて破棄 + 変更を絞り込み... 未追跡のファイルを含める 最近の入力メッセージはありません コミットテンプレートがありません @@ -964,9 +1061,12 @@ ワークスペース: ワークスペースの設定... 作業ツリー + ブランチ パスをコピー + HEAD ロック 開く + パス 削除 ロックを解除 はい diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index fa71db599f..e0695553fb 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -1,7 +1,11 @@ + + + 정보 SourceGit 정보 + 배포일: {0} 릴리스 노트 오픈소스 & 무료 Git GUI 클라이언트 무시할 파일 추가 @@ -12,21 +16,20 @@ 이 워크트리의 경로입니다. 상대 경로를 지원합니다. 브랜치 이름: 선택 사항. 기본값은 대상 폴더 이름입니다. - 추적할 브랜치: - 원격 브랜치 추적 - 체크아웃할 대상: - 새 브랜치 생성 - 기존 브랜치 AI 어시스턴트 모델 재생성 AI를 사용하여 커밋 메시지 생성 + 사용 SourceGit 숨기기 + 다른 항목 숨기기 모두 보기 - 패치 - 패치 파일: - 적용할 .patch 파일을 선택하세요 + 3방향 병합 + 적용할 패치 파일을 선택하세요 공백 변경 사항 무시 + 원본: + 파일 + 클립보드 패치 적용 공백: 스태시 적용 @@ -44,20 +47,29 @@ 변경되지 않음으로 간주된 파일 없음 이미지 불러오기... 새로 고침 - 바이너리 파일은 지원되지 않습니다!!! 이진 탐색 중단 나쁨 - 이진 탐색 중. 현재 HEAD가 '좋음' 상태입니까, '나쁨' 상태입니까? 좋음 건너뛰기 - 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. + 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. + 이진 탐색 중. 현재 HEAD가 '좋음' 상태입니까, '나쁨' 상태입니까? 블레임 - ${0}$ 체크아웃... + 이전 리비전으로 책임 추적 + 공백 변경 무시 + 이 파일은 책임 추적 기능을 지원하지 않습니다 + ${0}$(으)로 체크아웃... + 선택한 두 브랜치 비교하기 + 다음과 비교... + HEAD와 비교 + ${0}$와 비교 브랜치 이름 복사 + PR 생성 + 업스트림 ${0}$에 대한 PR 생성... 사용자 지정 작업 ${0}$ 삭제... 선택한 {0}개의 브랜치 삭제 + ${0}$ 설명 편집... ${0}$(으)로 Fast-Forward ${0}$에서 ${1}$(으)로 Fetch... Git Flow - ${0}$ 완료 @@ -66,7 +78,7 @@ 선택한 {0}개의 브랜치를 현재 브랜치로 병합 ${0}$ Pull ${0}$에서 ${1}$(으)로 Pull... - ${0}$ Push + ${0}$(으)로 Push ${1}$을(를) 기반으로 ${0}$ 리베이스... ${0}$ 이름 바꾸기... ${0}$을(를) ${1}$(으)로 리셋... @@ -85,6 +97,9 @@ 부모 리비전으로 리셋 이 리비전으로 리셋 커밋 메시지 생성 + 병합(내장) + 병합(외부 도구) + 파일을 ${0}$(으)로 되돌리기 표시 모드 변경 파일 및 디렉터리 목록으로 보기 경로 목록으로 보기 @@ -93,14 +108,15 @@ 서브모듈: URL: 브랜치 체크아웃 - 커밋 체크아웃 - 커밋: - 경고: 커밋 체크아웃을 하면, HEAD가 분리됩니다(detached) 로컬 변경 사항: 브랜치: 현재 HEAD에 브랜치/태그에 연결되지 않은 커밋이 있습니다! 계속하시겠습니까? + 다음 서브모듈을 업데이트해야 합니다:{0}업데이트하시겠습니까? 체크아웃 & Fast-Forward Fast-Forward 대상: + 스태시에서 브랜치 체크아웃 + 새로운 브랜치: + 스태시: 체리픽 커밋 메시지에 원본 추가 커밋: @@ -112,6 +128,8 @@ 원격 저장소 복제 추가 파라미터: 저장소 복제 시 추가 인수. 선택 사항. + 북마크: + 그룹: 로컬 이름: 저장소 이름. 선택 사항. 상위 폴더: @@ -119,45 +137,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 @@ -172,8 +191,14 @@ SHA 서명자: 브라우저에서 열기 + 커밋 메시지를 입력하세요. 제목과 설명은 빈 줄로 구분하세요! 제목 비교 + 변경점 + 커밋 + 왼쪽에만 있는 커밋 + 오른쪽에만 있는 커밋 + 팁: 반대편이 HEAD인 경우 선택한 커밋을 체리픽할 수 있습니다(컨텍스트 메뉴 사용). 저장소 설정 커밋 템플릿 ${files_num}, ${branch_name}, ${files} 및 ${files:N} (N은 출력할 최대 파일 경로 수)을(를) 사용할 수 있습니다. @@ -181,8 +206,8 @@ 템플릿 이름: 사용자 지정 작업 인수: - 내장 파라미터: - + 내장 파라미터: + ${REPO} 저장소 경로 ${REMOTE} 선택한 원격 또는 선택한 브랜치의 원격 ${BRANCH} 선택한 브랜치 (원격 브랜치의 경우 ${REMOTE} 부분 제외) @@ -206,8 +231,10 @@ 이메일 주소 이메일 주소 GIT + 서브모듈 자동 업데이트 전에 확인 원격 자동 Fetch 분 + Conventional Commit 유형 기본 원격 선호하는 병합 모드 이슈 트래커 @@ -241,7 +268,11 @@ 레이블: 옵션: 옵션 구분자로 '|'를 사용하세요 + 문자열 포매터: + 선택 사항. 출력 문자열 형식 지정에 사용합니다. 입력이 비어 있으면 무시됩니다. 입력 문자열은 `${VALUE}`로 표시하세요. + 여기에서도 내장 변수 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, ${TAG}를 사용할 수 있습니다 유형: + 표시용 이름 사용: 작업 공간 색상 이름 @@ -249,6 +280,7 @@ 계속 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty)? 모두 스테이징 & 커밋 + 선택 항목 스테이징 후 커밋 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty) 아니면 모두 스테이징 후 커밋하시겠습니까? 재시작 필요 변경 사항을 적용하려면 앱을 다시 시작해야 합니다. @@ -261,6 +293,7 @@ 변경 유형: 복사 전체 텍스트 복사 + 패치로 복사 전체 경로 복사 경로 복사 브랜치 생성... @@ -286,6 +319,7 @@ Ctrl을 누른 채 클릭하면 바로 시작합니다 잘라내기 폐기 + 아무것도 하지 않기 스태시 & 재적용 서브모듈 초기화 해제 로컬 변경 사항이 있어도 강제로 초기화 해제합니다. @@ -313,7 +347,6 @@ 태그: 원격 저장소에서도 삭제 바이너리 비교 - 파일 모드 변경됨 첫 번째 차이점 공백 변경 사항 무시 혼합 @@ -333,6 +366,7 @@ 서브모듈 삭제됨 신규 + 커밋 안 한 변경 사항 + {0}개 전환 구문 강조 줄 바꿈 @@ -349,12 +383,12 @@ 작업 사본의 모든 로컬 변경 사항. 변경 사항: 무시된 파일 포함 + 수정/삭제된 파일 포함 추적하지 않는 파일 포함 {0}개의 변경 사항이 폐기됩니다 이 작업은 되돌릴 수 없습니다!!! - 커밋 삭제 - 커밋: - 새 HEAD: + 브랜치 설명 수정 + 대상: 북마크: 새 이름: 대상: @@ -369,6 +403,7 @@ 원격: 원격 변경 사항 Fetch 변경되지 않음으로 간주 + 사용자 지정 작업 폐기... {0}개 파일 폐기... ${0}$을(를) 사용하여 해결 @@ -423,6 +458,8 @@ 내 잠금만 보기 LFS 잠금 잠금 해제 + 내 잠금 모두 해제 + 잠근 파일을 모두 해제하시겠습니까? 강제 잠금 해제 정리 로컬 저장소에서 오래된 LFS 파일을 삭제하려면 `git lfs prune`을 실행하세요 @@ -435,16 +472,25 @@ 원격: '{0}' 이름의 파일 추적 모든 *{0} 파일 추적 - 히스토리 + 커밋 선택 작성자 작성 시간 커밋 시간 그래프 & 제목 SHA + 그래프 강조 표시 + 모두 + 현재 브랜치만 + 현재 브랜치와 선택한 커밋 + 선택한 커밋만 {0}개 커밋 선택됨 + 표시할 역 'Ctrl' 또는 'Shift' 키를 누른 채로 여러 커밋을 선택하세요. ⌘ 또는 ⇧ 키를 누른 채로 여러 커밋을 선택하세요. 팁: + 별도의 창으로 열기 + 커밋 세부 정보 + 리비전 비교 키보드 단축키 참조 전역 새 저장소 복제 @@ -452,31 +498,31 @@ 다음 탭으로 이동 이전 탭으로 이동 새 탭 만들기 + 로컬 저장소 열기 환경설정 대화상자 열기 + 작업 공간 드롭다운 메뉴 열기 활성 탭 전환 + 확대 / 축소 저장소 스테이징된 변경 사항 커밋 스테이징된 변경 사항 커밋 및 푸시 모든 변경 사항 스테이징 후 커밋 + 브랜치 생성 Fetch (바로 시작) - 대시보드 모드 (기본) - 커밋 검색 모드 열기 + 선택한 커밋의 자식으로 이동 + 선택한 커밋의 부모로 이동 + 명령 팔레트 열기 Pull (바로 시작) 푸시 (바로 시작) 이 저장소 강제 새로고침 '변경 사항'으로 전환 - '히스토리'로 전환 '스태시'로 전환 - 텍스트 에디터 - 검색 패널 닫기 - 다음 일치 항목 찾기 - 이전 일치 항목 찾기 - 외부 diff/merge 도구로 열기 - 검색 패널 열기 폐기 스테이지 언스테이지 저장소 초기화 + 이 경로에서 `git init` 명령을 실행하시겠습니까? + 저장소를 열지 못했습니다. 원인: 경로: 체리픽 진행 중. 커밋 처리 중 @@ -488,13 +534,16 @@ 커밋 되돌리는 중 대화형 리베이스 로컬 변경 사항 스태시 & 재적용 + pre-rebase 훅 건너뛰기 기준: 드래그 앤 드롭으로 커밋 순서 변경 대상 브랜치: 링크 복사 브라우저에서 열기 + 명령 오류 알림 + 저장소 열기 탭 작업 공간 브랜치 병합 @@ -502,6 +551,26 @@ 대상: 병합 옵션: 소스: + 병합 가능 여부 확인 중... + 충돌 없이 병합할 수 있습니다 + 병합 테스트 중 알 수 없는 오류가 발생했습니다 + 병합 시 충돌이 발생합니다 + 내 변경 먼저, 상대 변경 나중 + 상대 변경 먼저, 내 변경 나중 + 둘 다 사용 + 모든 충돌 해결됨 + 남은 충돌 {0}개 + 내 변경 + 다음 충돌 + 이전 충돌 + 결과 + 저장 후 스테이징 + 상대 변경 + 병합 충돌 + 저장하지 않은 변경사항을 버리시겠습니까? + 내 변경 사용 + 상대 변경 사용 + 실행 취소 병합 (다중) 모든 변경 사항 커밋 전략: @@ -512,9 +581,17 @@ 저장소 노드 이동 상위 노드 선택: 이름: + 아니요 Git이 구성되지 않았습니다. [환경설정]으로 이동하여 먼저 구성하세요. + 열기 + 시스템 기본 편집기 데이터 저장 디렉터리 열기 + 파일 열기 병합 도구에서 열기 + 로컬 저장소 열기 + 북마크: + 그룹: + 폴더: 선택 사항. 새 탭 만들기 탭 닫기 @@ -522,6 +599,8 @@ 오른쪽 탭 닫기 저장소 경로 복사 편집 + 작업 공간으로 이동 + 새로 고침 저장소 붙여넣기 {0}일 전 @@ -536,7 +615,10 @@ 어제 환경설정 AI + 추가 프롬프트(`-`로 요구 사항 나열) API 키 + 모델 + 사용 가능한 모델 ID 자동 가져오기 이름 입력된 값은 환경변수(ENV)에서 API 키를 불러올 이름입니다 서버 @@ -550,8 +632,13 @@ 테마 테마 재정의 스크롤바 자동 숨기기 사용 + 제목 표시줄에서 고정 탭 너비 사용 네이티브 윈도우 프레임 사용 DIFF/MERGE 도구 + 비교 인수 + 사용 가능한 변수: $LOCAL, $REMOTE + 병합 인수 + 사용 가능한 변수: $BASE, $LOCAL, $REMOTE, $MERGED 설치 경로 diff/merge 도구 경로 입력 도구 @@ -561,12 +648,13 @@ 변경 사항 트리에서 폴더 압축 활성화 언어 히스토리 커밋 수 - 그래프에 커밋 시간 대신 작성자 시간 표시 기본으로 `로컬 변경 사항` 페이지 표시 커밋 세부 정보에서 기본으로 `변경 사항` 탭 표시 - 커밋 세부 정보에 자식 커밋 표시 + 커밋 그래프에 상대 시간 표시 커밋 그래프에 태그 표시 제목 가이드 길이 + 24시간 형식 + 커밋 그래프에서 축약 브랜치 이름 사용 GitHub 스타일 기본 아바타 생성 GIT 자동 CRLF 활성화 @@ -581,6 +669,7 @@ git-credential-manager 대신 git-credential-libsecret 사용 사용자 이름 전역 git 사용자 이름 + 브랜치 체크아웃 또는 병합 시 기본적으로 변경 사항을 스태시한 뒤 다시 적용 Git 버전 GPG 서명 커밋 GPG 서명 @@ -592,6 +681,8 @@ 사용자의 gpg 서명 키 연동 셸/터미널 + 인수 + 작업 디렉터리는 `.`으로 표시하세요 경로 셸/터미널 원격 정리 @@ -610,6 +701,7 @@ 강제 푸시 로컬 브랜치: 신규 + pre-push 훅 건너뛰기 원격: 리비전: 리비전을 원격에 푸시 @@ -621,10 +713,17 @@ 모든 원격에 푸시 원격: 태그: + 새 브랜치로 푸시 + 새 원격 브랜치 이름을 입력하세요: 종료 현재 브랜치 리베이스 로컬 변경 사항 스태시 & 재적용 + pre-rebase 훅 건너뛰기 기준: + 리베이스 가능 여부 확인 중... + 충돌 없이 리베이스할 수 있습니다 + 리베이스 테스트 중 알 수 없는 오류가 발생했습니다 + 리베이스 시 충돌이 발생합니다 원격 추가 원격 편집 이름: @@ -635,6 +734,7 @@ 사용자 지정 작업 삭제... 편집... + 자동 Fetch 사용 Fetch 브라우저에서 열기 정리 @@ -658,7 +758,6 @@ 계속 사용자 지정 작업 사용자 지정 작업 없음 - 대시보드 모든 변경 사항 폐기 파일 탐색기에서 열기 브랜치/태그/서브모듈 검색 @@ -673,18 +772,17 @@ 커밋 날짜 위상 정렬 로컬 브랜치 - 추가 옵션... HEAD로 이동 브랜치 생성 알림 지우기 - 현재 브랜치만 강조 + 폴더로 열기 {0}에서 열기 외부 도구에서 열기 원격 원격 추가 + 해결 커밋 검색 작성자 - 커밋터 내용 메시지 경로 @@ -707,7 +805,6 @@ 이름 순 정렬 터미널에서 열기 - 상대 시간 사용 로그 보기 브라우저에서 '{0}' 방문 워크트리 @@ -725,7 +822,6 @@ 커밋 되돌리기 커밋: 되돌린 변경 사항 커밋 - 커밋 메시지 수정 실행 중. 잠시만 기다려주세요... 저장 다른 이름으로 저장... @@ -735,9 +831,11 @@ 다른 사용자 정의 디렉터리 스캔 업데이트 확인... 이 소프트웨어의 새 버전을 사용할 수 있습니다: + 현재 버전: 업데이트 확인 실패! 다운로드 이 버전 건너뛰기 + 새 버전 릴리스 날짜: 소프트웨어 업데이트 현재 사용 가능한 업데이트가 없습니다. 서브모듈 브랜치 설정 @@ -763,6 +861,8 @@ 선택한 파일의 스테이징된 변경 사항과 스테이징되지 않은 변경 사항이 모두 스태시됩니다!!! 로컬 변경 사항 스태시 적용 + 변경 사항 적용 + 새 브랜치 체크아웃 메시지 복사 삭제 패치로 저장... @@ -796,11 +896,17 @@ 수정됨 초기화 안 됨 리비전 변경됨 + 병합되지 않음 업데이트 URL + 서브모듈 변경 상세 + 상세 열기 확인 태그 생성자 시간 + 태그 2개 비교 + 다음과 비교... + HEAD와 비교 메시지 이름 태그 생성자 @@ -808,7 +914,6 @@ 사용자 지정 작업 ${0}$ 삭제... 선택한 {0}개의 태그 삭제... - ${0}$을(를) ${1}$(으)로 병합... ${0}$ 푸시... 서브모듈 업데이트 모든 서브모듈 @@ -853,6 +958,8 @@ 분리된(detached) HEAD에 커밋을 생성하고 있습니다. 계속하시겠습니까? {0}개의 파일을 스테이징했지만 {1}개의 파일만 표시됩니다 ({2}개의 파일은 필터링됨). 계속하시겠습니까? 충돌 감지됨 + 병합 + 외부 도구로 병합 모든 충돌을 외부 병합 도구에서 열기 파일 충돌 해결됨 내 것 사용 @@ -874,9 +981,13 @@ 작업 공간: 작업 공간 설정... 워크트리 + 브랜치 경로 복사 + HEAD 잠금 열기 + 경로 제거 잠금 해제 + 예 diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 8c2f112052..5084f0180b 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -16,20 +16,13 @@ Caminho para este worktree. Caminho relativo é suportado. Nome do Branch: Opcional. O padrão é o nome da pasta de destino. - Rastrear Branch: - Rastreando branch remoto - O que Checar: - Criar Novo Branch - Branch Existente Assietente IA Modelo Gerar novamente Utilizar IA para gerar mensagem de commit Esconder SourceGit Mostrar Todos - Patch - Arquivo de Patch: - Selecione o arquivo .patch para aplicar + Selecione o arquivo patch para aplicar Ignorar mudanças de espaço em branco Aplicar Patch Espaço em Branco: @@ -48,14 +41,13 @@ NENHUM ARQUIVO CONSIDERADO SEM ALTERAÇÕES Carregar imagem... Atualizar - ARQUIVO BINÁRIO NÃO SUPORTADO!!! Bisect Abortar Inválido - Bisect. O HEAD atual é válido ou inválido? Válido Pular - Bisect. Marcar o commit atual como válido ou inválido e checar outro commit. + 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 Ignorar espaços em branco BLAME NESSE ARQUIVO NÃO É SUPORTADO!!! @@ -96,9 +88,6 @@ 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: Branch: Cherry-Pick @@ -126,9 +115,7 @@ SHA Ação customizada Reverter Commit - Modificar Mensagem Salvar como Patch... - Mesclar ao Commit Pai ALTERAÇÕES Buscar Alterações... ARQUIVOS @@ -236,7 +223,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 @@ -336,7 +322,6 @@ Remoto: Rastrear arquivos nomeados '{0}' Rastrear todos os arquivos *{0} - Históricos AUTOR DATA DO AUTOR HORA DO COMMIT @@ -358,19 +343,11 @@ Commitar e enviar mudanças preparadas Preparar todas as mudanças e commitar Buscar, imediatamente - Modo de Dashboard (Padrão) - Modo de busca de commits Puxar, imediatamente Enviar, imediatamente Forçar recarregamento deste repositório 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 @@ -439,7 +416,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 @@ -479,6 +455,7 @@ Certifica de que submodules foram enviadas Forçar push Branch Local: + Ignorar o hook de pre-push Remoto: Empurrar Alterações para o Remoto Branch Remoto: @@ -537,7 +514,6 @@ ADICIONAR REMOTO Pesquisar Commit Autor - Committer Mensagem SHA Branch Atual @@ -561,7 +537,6 @@ Reverter Commit Commit: Commitar alterações de reversão - Reescrever Mensagem do Commit Executando. Por favor, aguarde... SALVAR Salvar Como... @@ -609,7 +584,6 @@ Excluir Submódulo OK Excluir ${0}$... - Mesclar ${0}$ em ${1}$... Enviar ${0}$... Atualizar Submódulos Todos os submódulos diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 570a3bf2e8..f9260cc4f9 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -16,11 +16,6 @@ Путь к рабочему каталогу (поддерживается относительный путь) Имя ветки: Имя целевого каталога по умолчанию (необязательно) - Отслеживание ветки: - Отслеживание внешней ветки - Переключиться на: - Создать новую ветку - Ветку из списка Помощник OpenAI Модель ПЕРЕСОЗДАТЬ @@ -29,11 +24,12 @@ Скрыть SourceGit Скрыть остальные Показать все - Исправить Трехстороннее слияние - Файл заплатки: - Выберите файл .patch для применения + Выберите файл заплатку для применения Игнорировать изменения пробелов + Источник: + Файл + Буфер обмена Применить заплатку Пробел: Отложить @@ -51,22 +47,26 @@ СПИСОК ПУСТ Загрузить картинку... Обновить - ДВОИЧНЫЙ ФАЙЛ НЕ ПОДДЕРЖИВАЕТСЯ!!! Раздвоить О Плохая - Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая? Хорошая Пропустить - Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой. + Раздвоение. Пожалуйста, переключитесь на другой, затем отметьте его как хороший или плохой. + Раздвоение. Отметить текущую ревизию как плохую или переключиться на другую, а затем отметить её как плохую. + Раздвоение. Отметить текущую ревизию хорошей или плохой и переключиться на другой. + Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая? Расследование Расследование на предыдущей редакции Игнорировать изменения пробелов + Если при нажатии на ревизию SHA удерживать нажатой Ctrl (на Windows/Linux) или ⌘ (на macOS), данные расследования будут перестроены на основе этой ревизии. РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! Переключиться на ${0}$... + Сделать ${0}$ рабочим деревом... Сравнить две выбранные ветки Сравнить с ... Сравнить с ГОЛОВОЙ + Сравнить с ${0}$ Копировать имя ветки Создать PR... Создать PR для основной ветки ${0}$... @@ -92,6 +92,7 @@ {0} ревизий вперёд, {1} ревизий назад {0} ревизий назад Неверно + АДРЕС ВЫГРУЗКИ УДАЛЁННЫЙ СОСТОЯНИЕ ОТСЛЕЖИВАНИЕ @@ -112,18 +113,26 @@ Подмодуль: URL-адрес: Переключить ветку - Переключение ревизии - Ревизия: - Предупреждение: После переключения ревизии ваша Голова (HEAD) будет отсоединена Локальные изменения: Ветка: Ваша текущая ГОЛОВА содержит ревизию(и), не связанные ни с к какими ветками или метками! Вы хотите продолжить? Подмодулям требуется обновление:{0}Обновить их? Переключиться и перемотать Перемотать к: + Сделать ветку рабочим деревом + Локальная ветка: + Имя новой ветки: + По желанию. Используемое имя целевого каталога по умолчанию. + Удалённая ветка: + Отслеживать эту удалённую ветку. + Путь рабочего дерева: + Путь для данного рабочего дерева. Поддерживается относительный путь. Переключить ветку из отложенного Новая ветка: Отложенный: + Переключить ревизию или метку + Цель: + Предупреждение: После этого ГОЛОВА будет отсоединена Частичный выбор Добавить источник для ревизии сообщения Ревизия(и): @@ -154,12 +163,13 @@ Сравнить c ГОЛОВОЙ (HEAD) Сравнить с рабочим каталогом Автор + Автор (время) Сообщение Ревизор + Ревизор (время) SHA Субъект Пользовательское действие - Бросить ревизию Интерактивное перемещение Бросить... Редактировать... @@ -167,26 +177,23 @@ Интерактивное перемещение ${0}$ в ${1}$ Изменить комментарий... Втиснуть в родительский... - Влить в ${0}$ Влить ... Выложить ${0}$ в ${1}$ Переместить ${0}$ в ${1}$ Сбросить ${0}$ к ${1}$ Отменить ревизию - Изменить комментарий Сохранить как заплатки... - Объединить с предыдущей ревизией - Исправить в родительском ИЗМЕНЕНИЯ изменённый(х) файл(ов) Найти изменения.... + Свернуть подробности ФАЙЛЫ Файл LFS Поиск файлов... Подмодуль + ВЫБЕРИТЕ ФАЙЛ ДЛЯ ПРОСМОТРА СОДЕРЖИМОГО ИНФОРМАЦИЯ АВТОР - ДОЧЕРНИЙ РЕВИЗОР (ИСПОЛНИТЕЛЬ) Найти все ветки с этой ревизией ВЕТКИ С ЭТОЙ РЕВИЗИЕЙ @@ -201,29 +208,32 @@ SHA Подписант: Открыть в браузере - СТБ Введите сообщение ревизии. Пожалуйста, используйте пустые строки для разделения субъекта и описания! СУБЪЕКТ Сравнить - Сравнить с ГОЛОВОЙ + ИЗМЕНЕНИЯ + РЕВИЗИИ + РЕВИЗИИ ТОЛЬКО СЛЕВА + РЕВИЗИИ ТОЛЬКО СПРАВА + Подсказка: Если другая сторона является ГОЛОВОЙ (используя контекстное меню), вы можете использовать частично выбранные ревизии (cherry-pick). Настройка репозитория ШАБЛОН РЕВИЗИИ - Встроенные параметры: - + Встроенные параметры: + ${branch_name} Имя текущей локальной ветки ${files_num} Количество изменённых файлов ${files} Пути изменённых файлов ${files:N} Пути изменённых файлов, не более N ${pure_files} То же, что и ${files}, но только имена файлов ${pure_files:N} То же, что и ${files:N}, но только имена файлов - Cодержание: + Содержание: Название: ПОЛЬЗОВАТЕЛЬСКОЕ ДЕЙСТВИЕ Аргументы: - Встроенные параметры: - + Встроенные параметры: + ${REPO} Путь репозитория - ${REMOTE} Выбранная удаённая ветка + ${REMOTE} Выбранная удалённая ветка ${BRANCH} Выбранная ветка, без ${REMOTE} удалённых веток ${BRANCH_FRIENDLY_NAME} Понятное имя выбранной ветки, содержащую ${REMOTE} удалённые ветки ${SHA} Хеш выбранной ревизии @@ -241,7 +251,7 @@ Удалённый Репозиторий Метка - Ждать для выполения выхода + Ждать для выполнения выхода Адрес электронной почты Адрес электронной почты GIT @@ -250,6 +260,7 @@ Минут(а/ы) Общепринятые типы ревизии Внешний репозиторий по умолчанию + Включить (--recursive) при автоматическом обновлении подмодулей Предпочтительный режим слияния ОТСЛЕЖИВАНИЕ ПРОБЛЕМ Добавить пример правила Azure DevOps @@ -307,6 +318,7 @@ Тип изменения: Копировать Копировать весь текст + Копировать как исправление Копировать полный путь Копировать путь Создать ветку... @@ -331,14 +343,16 @@ Простой Удерживайте Ctrl, чтобы сразу начать Вырезать + Обзор Отклонить Ничего не делать - Отложить и примненить повторно + Отложить и применить повторно Удалить подмодуль Принудительно удалить даже если содержит локальные изменения. Подмодуль: Удалить ветку Ветка: + Принудительно удалить, даже если содержит неслитые ревизии Вы собираетесь удалить внешнюю ветку!!! Также удалите внешнюю ветку ${0}$ Удаление нескольких веток @@ -360,7 +374,8 @@ Метка: Удалить из внешнего репозитория СРАВНЕНИЕ БИНАРНИКОВ - Режим файла изменён + ПРОСМОТР НОВОГО СОДЕРЖИМОГО + ФАЙЛ ПУСТ Первое сравнение Игнорировать изменения пробелов СМЕСЬ @@ -380,7 +395,7 @@ ПОДМОДУЛЬ УДАЛЁН НОВЫЙ - + {0} Незафиксированных изменний + + {0} Незафиксированных изменений Обмен Подсветка синтаксиса Перенос слов в строке @@ -401,9 +416,6 @@ Включить неотслеживаемые файлы {0} изменений будут отменены Вы не можете отменить это действие!!! - Бросить ревизию - Ревизия: - Новая ГОЛОВА: Править описание ветки Цель: Закладка: @@ -419,6 +431,9 @@ Извлечь без меток Внешний репозиторий: Извлечь внешние изменения + ДВОИЧНЫЙ ФАЙЛ + ПРОСМОТР В ШЕСТНАДЦАТЕРИЧНОМ ФОРМАТЕ + Перейти по адресу... Не отслеживать Пользовательское действие Отклонить... @@ -436,14 +451,25 @@ История файлов ИЗМЕНИТЬ СОДЕРЖИМОЕ + Изменение режима файла: + Режим удалённого файла: + Каталог + Исполняемый + Новый режим файла: + Обычный + Подмодуль + Символическая ссылка + Неизвестно Git-процесс Ветка разработчика: Свойство: Свойство префикса: + Завершение ${0}$ ПРОЦЕСС - Свойства завершения ПРОЦЕСС - Закончить исправление ПРОЦЕСС - Завершить выпуск Цель: + Перенос перед слиянием Втиснуть при слиянии Исправление: Префикс исправлений: @@ -452,10 +478,12 @@ Производственная ветка: Выпуск: Префикс выпуска: + Начать с: Свойство запуска... ПРОЦЕСС - Свойство запуска Запуск исправлений... ПРОЦЕСС - Запуск исправлений + Имя: Ввести имя Запуск выпуска... ПРОЦЕСС - Запуск выпуска @@ -490,18 +518,26 @@ Отслеживать файлы с именем «{0}» Отслеживать все файлы (*{0}) Выбрать ревизию - Истории + Расширенные опции АВТОР ВРЕМЯ АВТОРА ВРЕМЯ РЕВИЗИИ - ДАТА И ВРЕМЯ ГРАФ И СУБЪЕКТ SHA + ПОДСВЕТКА В ГРАФЕ + ВСЕ + Только текущая ветка + Текущая ветка и выбранные ревизии + Только выбранные ревизии + Выбранные ревизии (Только первый родитель) ВЫБРАНО РЕВИЗИЙ: {0} ПОКАЗЫВАТЬ КОЛОНКИ Удерживайте Ctrl или Shift, чтобы выбрать несколько ревизий. Удерживайте ⌘ или ⇧, чтобы выбрать несколько ревизий. ПОДСКАЗКИ: + Открыть в отдельном окне + Подробности ревизии + Сравнить ревизию Справка по сочетаниям клавиш ГЛОБАЛЬНЫЕ Клонировать репозиторий @@ -511,7 +547,7 @@ Создать новую вкладку Открыть локальный репозиторий Открыть диалоговое окно настроек - Показать рабочее пространство в выпадющем меню + Показать рабочее пространство в выпадающем меню Переключиться на вкладку Масштабирование РЕПОЗИТОРИЙ @@ -520,40 +556,35 @@ Сформировать все изменения и зафиксировать Создать новую ветку Извлечь (fetch), запускается сразу - Режим доски (по умолчанию) К дочернему выбранной ревизии К родительскому выбранной ревизии Открыть палитру команд - Режим поиска ревизий Загрузить (pull), запускается сразу Выложить (push), запускается сразу Принудительно перечитать репозиторий + Развернуть/Свернуть панель подробностей ревизии + Переключить панель поиска ревизий Переключить на «Изменения» - Переключить на «Историю» + Переключить на «Обзор» Переключить на «Отложенные» - ТЕКСТОВЫЙ РЕДАКТОР - Закрыть панель поиска - Найти следующее совпадение - Найти предыдущее совпадение - Открыть во внешнем инструменте сравнения/слияния - Открыть панель поиска Отклонить Сформировать Расформировать Создать репозиторий - Вы действительно хотите запуститб команду «git init» по этому пути? + Вы действительно хотите запустить команду «git init» по этому пути? Не удалось открыть репозиторий. Причина: Путь: Выполняется частичный перенос ревизий (cherry-pick). - Обрабтка ревизии. + Обработка ревизии. Выполняется слияние. - Выполяется. + Выполняется. Выполняется перенос. Остановлен на Выполняется отмена ревизии. Выполняется отмена Интерактивное перемещение Отложить и применить повторно локальные изменения + Обходной зацеп предварительного перемещения На: Перетаскивайте для переупорядочивания ревизий Целевая ветка: @@ -562,6 +593,7 @@ Команды ОШИБКА УВЕДОМЛЕНИЕ + Доступна новая версия Открыть репозитории Вкладки Рабочие места @@ -570,6 +602,10 @@ В: Опции слияния: Источник: + Тестировать на слияние... + Слияние может быть выполнено без конфликтов + Неизвестная ошибка при тестировании слияния + Слияние вызовет конфликты Сначала мои, а потом их Сначала их, а потом мои ИСПОЛЬЗОВАТЬ ОБА @@ -601,6 +637,8 @@ Открыть Редактор по умолчанию (Системный) Открыть каталог данных программы + Кеш + Конфигурация Открыть файл Открыть в инструменте слияния Открыть локальный репозиторий @@ -629,11 +667,15 @@ {0} лет назад Вчера Параметры - ОТКРЫТЬ ИИ + ИИ Дополнительная подсказка (Для перечисления ваших требований используйте `-`) Ключ API + Дополнительные заголовки (по одному «Имя: Значение» на строку) + Модель + Автоматическое получение доступных идентификаторов моделей Имя: Введённое значение — это имя для загрузки API-ключа из ENV + Уровень обоснованности усилий Сервер ВИД Шрифт по умолчанию @@ -641,7 +683,7 @@ Размер шрифта По умолчанию Редактор - Моноширный шрифт + Моноширинный шрифт Тема Переопределение темы Автоматически скрывать прокрутку @@ -661,19 +703,19 @@ Включить компактные каталоги в дереве изменений Язык Максимальная длина истории - Показывать время автора вместо времени ревизии на графике Показывать вкладку «ЛОКАЛЬНЫЕ ИЗМЕНЕНИЯ» по умолчанию Показывать вкладку «Изменения» в сведении ревизии по умолчанию - Показать наследника в деталях комментария + Отображение относительного времени на графе ревизии Показывать метки на графике Длина темы ревизии 24-часовой + Использовать компактные имена веток в графе ревизии Создать Github-подобный аватар по умолчанию GIT Включить автозавершение CRLF Каталог клонирования по умолчанию - Электроная почта пользователя - Общая электроная почта пользователя git + Электронная почта пользователя + Общая электронная почта пользователя git Разрешить (--prune) при скачивании Разрешить (--ignore-cr-at-eol) в сравнении Для работы программы требуется версия Git (>= 2.25.1) @@ -682,6 +724,7 @@ Использовать git-credential-libsecret вместо git-credential-manager Имя пользователя Общее имя пользователя git + Отложить и повторно применить изменения по умолчанию при извлечении или объединении веток Версия Git GPG ПОДПИСЬ GPG подпись ревизии @@ -713,9 +756,10 @@ Принудительно выложить Локальная ветка: НОВЫЙ + Обходной зацеп предварительного выкладывания Внешний репозиторий: Ревизия: - Выложить ревизию на удалёную + Выложить ревизию на удалённую Выложить изменения на внешний репозиторий Ветка внешнего репозитория: Отслеживать ветку @@ -725,15 +769,21 @@ Внешний репозиторий: Метка: Отправить к НОВОЙ ветке - Введитте имя для новой удалённой ветки: + Введите имя для новой удалённой ветки: Выйти Перемещение текущей ветки Отложить и применить повторно локальные изменения + Обходной зацеп предварительного перемещения На: + Тестировать на перемещение... + Перемещение может быть выполнено без конфликтов + Неизвестная ошибка при тестировании перемещения + Перемещение вызовет конфликты Добавить внешний репозиторий Редактировать внешний репозиторий Имя: Имя внешнего репозитория + Установить адрес выгрузки равным адресу этого репозитория Адрес: Адрес внешнего репозитория git Копировать адрес @@ -744,6 +794,7 @@ Извлечь Открыть в браузере Удалить + Установить адрес выгрузки... Подтвердить удаление рабочего каталога Включить опцию (--force) Цель: @@ -764,14 +815,16 @@ ПРОДОЛЖИТЬ Изменить действия Не изменять действия - Панель Отклонить все изменения. Открыть в файловом менеджере Поиск веток, меток и подмодулей Видимость на графике + Свернуть фильтры Не установлен (по умолчанию) Скрыть в графе ревизии + Развернуть фильтры Фильтр в графе ревизии + фильтры включены РАСПОЛОЖЕНИЕ Горизонтально Вертикально @@ -779,11 +832,9 @@ Дата ревизии Топологически ЛОКАЛЬНЫЕ ВЕТКИ - Больше опций... Навигация по ГОЛОВЕ (HEAD) Создать ветку ОЧИСТКА УВЕДОМЛЕНИЙ - Подсвечивать только текущую ветку Открыть как каталог Открыть в {0} Открыть в расширенном инструменте @@ -792,20 +843,21 @@ РАЗРЕШИТЬ Поиск ревизии Автор - Ревизор Содержимое Сообщение Путь SHA Текущая ветка + ИСКАТЬ В + Поиск ревизий Только оформленные ревизии Показывать только первый родительский ПОКАЗЫВАТЬ ФЛАГИ Показывать потерянные ревизии Показывать подмодули как дерево - Показывать метки как катлог + Показывать метки как каталог ПРОПУСТИТЬ - Статистикa + Статистика ПОДМОДУЛИ ДОБАВИТЬ ПОДМОДУЛЬ ОБНОВИТЬ ПОДМОДУЛЬ @@ -815,7 +867,6 @@ По имени Сортировать Открыть в терминале - Использовать относительное время Просмотр журналов Посетить '{0}' в браузере РАБОЧИЕ КАТАЛОГИ @@ -833,7 +884,6 @@ Отменить ревизию Ревизия: Отмена ревизии - Изменить комментарий ревизии Запуск. Подождите пожалуйста... СОХРАНИТЬ Сохранить как... @@ -850,6 +900,10 @@ Дата выпуска новой версии: Обновление ПО Сейчас нет обновлений. + Установить адрес выгрузки на удалённый + Адрес выгрузки: + Удалённый: + Адрес: Установить ветку подмодуля Подмодуль: Текущий: @@ -861,11 +915,24 @@ Основная ветка: Копировать SHA Перейти - Втиснуть ГОЛОВУ (HEAD) в родительскую - Исправить ГОЛОВУ (HEAD) в родительском - В: Приватный ключ SSH: - Путь хранения приватного ключа SSH + Путь хранения частного SSH-ключа + Открыть помощника SSH-ключей + Вы действительно хотите удалить следующие ключи? + Отпечаток + Создать SSH-ключ + Подтвердить парольную фразу: + Почта: + Требуется. + Имя ключа: + Требуется. К примеру: id_github + Парольная фраза: + СОЗДАТЬ + Тип: + Использовать парольную фразу для частного ключа + Путь + Открытый ключ + Помощник SSH-ключа ЗАПУСК Отложить Включить неотслеживаемые файлы @@ -887,9 +954,15 @@ ИЗМЕНЕНИЯ ОТЛОЖЕННЫЕ Статистика + РЕВИЗИИ ОБЗОР МЕСЯЦ НЕДЕЛЯ + SourceGit проанализирует не более 200 000 ревизий в целевой ветке. + ТОП АВТОРОВ + РЕВИЗИИ + ПРОЦЕНТ + Авторы с долей менее 1% ревизий объединены в группу «Другие». АВТОРЫ: РЕВИЗИИ: ПОДМОДУЛИ @@ -919,6 +992,7 @@ ОК РАЗМЕТЧИК ВРЕМЯ + Переключить метку... Сравнить две ветки Сравнить с ... Сравнить с ГОЛОВОЙ @@ -931,10 +1005,17 @@ Удалить выбранные метки ({0})... Влить ${0}$ в ${1}$... Выложить ${0}$... + ПРЕКРАТИТЬ + Недоверенный репозиторий + Вы хотите выполнить команду (git config --global --add safe.directory {0}), чтобы разрешить доступ к этому пути? + Отказ гита в доступе к этому репозиторию: + Путь: + Пропущено репозиториев из-за недоверия к ним гитом: {0}. Пожалуйста, откройте их вручную и проверьте, можно ли им доверять. Обновление подмодулей Все подмодули Создавать по необходимости Подмодуль: + Обновить вложенные подмодули Обновить до отслеживаемой ветки удалённого подмодуля Сетевой адрес: Журналы @@ -961,10 +1042,12 @@ Игнорировать *{0} файлы в том же каталоге Игнорировать неотслеживаемые файлы в этом каталоге Игнорировать только эти файлы + Игнорировать все неотслеживаемые файлы в одном и том же каталоге Изменить Теперь вы можете сформировать этот файл. Очистить историю Вы действительно хотите очистить всю историю сообщений ревизии? Данное действие нельзя отменить. + Скрыть боковую панель ЗАФИКСИРОВАТЬ ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории @@ -980,6 +1063,9 @@ КОНФЛИКТЫ ФАЙЛОВ РАЗРЕШЕНЫ ИСПОЛЬЗОВАТЬ МОИ ИСПОЛЬЗОВАТЬ ИХ + ОТКЛОНИТЬ ВСЕ + Показать боковую панель + Фильтр изменений... ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ НЕТ ШАБЛОНОВ РЕВИЗИИ diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 1dea63e929..3473ff1cab 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -11,17 +11,10 @@ இந்த பணிமரத்திற்கான பாதை. தொடர்புடைய பாதை ஆதரிக்கப்படுகிறது. கிளை பெயர்: விருப்பத்தேர்வு. இயல்புநிலை இலக்கு கோப்புறை பெயர். - கிளை கண்காணி: - தொலை கிளையைக் கண்காணித்தல் - என்ன சரிபார்க்க வேண்டும்: - புதிய கிளையை உருவாக்கு - ஏற்கனவே உள்ள கிளை செநு உதவியாளர் மாதிரி மறு-உருவாக்கு உறுதிமொழி செய்தியை உருவாக்க செநுவைப் பயன்படுத்து - ஒட்டு - ஒட்டு கோப்பு: .ஒட்டு இடுவதற்கு கோப்பைத் தேர்ந்தெடு வெள்ளைவெளி மாற்றங்களைப் புறக்கணி ஒட்டு இடு @@ -39,7 +32,6 @@ கோப்புகள் மாற்றப்படவில்லை எனக் கருதப்படுகிறது எந்த கோப்புகளும் மாற்றப்படவில்லை எனக் கருதப்படுகிறது புதுப்பி - இருமம் கோப்பு ஆதரிக்கப்படவில்லை!!! குற்றச்சாட்டு இந்த கோப்பில் குற்றம் சாட்ட ஆதரிக்கப்படவில்லை!!! ${0}$ சரிபார்... @@ -67,9 +59,6 @@ பாதை பட்டியலாகக் காட்டு கோப்பு முறைமை மரமாகக் காட்டு கிளை சரிபார் - உறுதிமொழி சரிபார் - உறுதிமொழி: - முன்னறிவிப்பு: ஒரு உறுதிமொழி சரிபார்பதன் மூலம், உங்கள் தலை பிரிக்கப்படும் உள்ளக மாற்றங்கள்: கிளை: கனி பறி @@ -97,12 +86,9 @@ பணிமரத்துடன் ஒப்பிடுக பாகொவ-வை தனிப்பயன் செயல் - ${0}$ இதற்கு ஒன்றிணை ஒன்றிணை ... உறுதிமொழி திரும்பபெறு - வேறுமொழி ஒட்டாக சேமி... - பெற்றோர்களில் நொறுக்கு மாற்றங்கள் மாற்றங்களைத் தேடு... கோப்புகள் @@ -111,7 +97,6 @@ துணைத்தொகுதி தகவல் ஆசிரியர் - குழந்தைகள் உறுதிமொழியாளர் இந்த உறுதிமொழிடைக் கொண்ட குறிப்புகளைச் சரிபார் உறுதிமொழி இதில் உள்ளது @@ -219,7 +204,6 @@ குறிசொல்: தொலை களஞ்சியங்களிலிருந்து நீக்கு இருமம் வேறுபாடு - கோப்பு முறை மாற்றப்பட்டது முதல் வேறுபாடு வெள்ளைவெளி மாற்றத்தை புறக்கணி கடைசி வேறுபாடு @@ -323,7 +307,6 @@ தொலை: '{0}' என பெயரிடப்பட்ட கோப்புகளைக் கண்காணி அனைத்து *{0} கோப்புகளையும் கண்காணி - வரலாறு ஆசிரியர் ஆசிரியர் நேரம் உறுதிமொழி நேரம் @@ -346,19 +329,11 @@ நிலைபடுத்திய மாற்றங்களை உறுதிமொழி மற்றும் தள்ளு அனைத்து மாற்றங்களையும் நிலைபடுத்தி உறுதிமொழி எடு, நேரடியாகத் தொடங்குகிறது - முகப்பலகை பயன்முறை (இயல்புநிலை) - உறுதிமொழி தேடல் பயன்முறை இழு, நேரடியாகத் தொடங்குகிறது தள்ளு, நேரடியாகத் தொடங்குகிறது இந்த களஞ்சியத்தை மீண்டும் ஏற்ற கட்டாயப்படுத்து 'மாற்றங்கள்' என்பதற்கு மாறு - 'வரலாறுகள்' என்பதற்கு மாறு 'பதுகிவைத்தவை' என்பதற்கு மாறு - உரை திருத்தி - தேடல் பலகத்தை மூடு - அடுத்த பொருத்தத்தைக் கண்டறி - முந்தைய பொருத்தத்தைக் கண்டறி - தேடல் பலகத்தைத் திற நிராகரி நிலைபடுத்து நிலைநீக்கு @@ -438,8 +413,6 @@ தேதி வடிவம் மொழி வரலாற்று உறுதிமொழிகள் - வரைபடத்தில் உறுதிமொழி நேரத்திற்குப் பதிலாக ஆசிரியர் நேரத்தைக் காட்டு - உறுதிமொழி விவரங்களில் குழந்தைகளைக் காட்டு உறுதிமொழி வரைபடத்தில் குறிச்சொற்களைக் காட்டு பொருள் வழிகாட்டி நீளம் அறிவிலி @@ -481,6 +454,7 @@ துணைத் தொகுதிகள் தள்ளப்பட்டது என்பதை உறுதிசெய் கட்டாயமாக தள்ளு உள்ளக கிளை: + pre-push hook-ஐத் தவிர் தொலை: மாற்றங்களை தொலைக்கு தள்ளு தொலை கிளை: @@ -545,7 +519,6 @@ தொலையைச் சேர் உறுதிமொழி தேடு ஆசிரியர் - உறுதிமொழியாளர் செய்தி பாகொவ தற்போதைய கிளை @@ -573,7 +546,6 @@ பின்வாங்கு உறுதிமொழி உறுதிமொழி: பின்வாங்கு மாற்றங்களை உறுதிமொழி - மாறுசொல் உறுதிமொழி செய்தி இயங்குகிறது. காத்திருக்கவும்... சேமி எனச் சேமி... @@ -627,7 +599,6 @@ துணை தொகுதியை நீக்கு சரி நீக்கு ${0}$... - ${0}$ இதை ${1}$ இல் இணை... தள்ளு ${0}$... துணைத்தொகுதிகளைப் புதுப்பி அனைத்து துணைத்தொகுதிகள் diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index e21cc44c97..5f44193e39 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -11,17 +11,10 @@ Шлях для цього робочого дерева. Відносний шлях підтримується. Назва гілки: Необов'язково. За замовчуванням — назва кінцевої папки. - Відстежувати гілку: - Відстежувати віддалену гілку - Що перемкнути: - Створити нову гілку - Наявна гілка AI Асистент Модель ПЕРЕГЕНЕРУВАТИ Використати AI для генерації повідомлення коміту - Застосувати - Файл патчу: Виберіть файл .patch для застосування Ігнорувати зміни пробілів Застосувати Патч @@ -39,7 +32,6 @@ ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ НЕМАЄ ФАЙЛІВ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ Оновити - БІНАРНИЙ ФАЙЛ НЕ ПІДТРИМУЄТЬСЯ!!! Автор рядка ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! Перейти на ${0}$... @@ -67,9 +59,6 @@ Показати як список шляхів Показати як дерево файлової системи Перейти на гілку - Перейти на коміт - Коміт: - Попередження: Перехід на коміт призведе до стану "від'єднаний HEAD" Локальні зміни: Гілка: Cherry-pick @@ -97,12 +86,9 @@ Порівняти з робочим деревом SHA Спеціальна дія - Злиття в ${0}$ Злити ... Скасувати коміт - Змінити повідомлення Зберегти як патч... - Склеїти з батьківським комітом ЗМІНИ Пошук змін... ФАЙЛИ @@ -111,7 +97,6 @@ Підмодуль ІНФОРМАЦІЯ АВТОР - ДОЧІРНІ КОМІТЕР Перевірити посилання, що містять цей коміт КОМІТ МІСТИТЬСЯ В @@ -223,7 +208,6 @@ Тег: Видалити з віддалених сховищ РІЗНИЦЯ ДЛЯ БІНАРНИХ ФАЙЛІВ - Змінено режим файлу Перша відмінність Ігнорувати зміни пробілів Остання відмінність @@ -327,7 +311,6 @@ Віддалений: Відстежувати файли, названі '{0}' Відстежувати всі *{0} файли - ІСТОРІЯ АВТОР ЧАС АВТОРА ЧАС КОМІТУ @@ -350,19 +333,11 @@ Закомітити та надіслати проіндексовані зміни Індексувати всі зміни та закомітити Fetch, запускається без діалогу - Режим панелі керування (за замовчуванням) - Режим пошуку комітів Pull, запускається без діалогу Push, запускається без діалогу Примусово перезавантажити це сховище Перейти до 'Зміни' - Перейти до 'Історія' Перейти до 'Схованки' - ТЕКСТОВИЙ РЕДАКТОР - Закрити панель пошуку - Знайти наступний збіг - Знайти попередній збіг - Відкрити панель пошуку Скасувати Індексувати Видалити з індексу @@ -442,8 +417,6 @@ Формат дати Мова Кількість комітів в історії - Показувати час автора замість часу коміту в графі - Показувати дочірні коміти в деталях Показувати теги в графі комітів Довжина лінії-орієнтира для теми GIT @@ -485,6 +458,7 @@ Переконатися, що підмодулі надіслано Примусовий push Локальна гілка: + Обхід pre-push hook Віддалене сховище: Надіслати зміни на віддалене сховище Віддалена гілка: @@ -549,7 +523,6 @@ ДОДАТИ ВІДДАЛЕНЕ СХОВИЩЕ Пошук коміту Автор - Комітер Повідомлення SHA Поточна гілка @@ -577,7 +550,6 @@ Revert (Скасувати коміт) Коміт: Закомітити зміни скасування - Змінити повідомлення коміту Виконується. Будь ласка, зачекайте... ЗБЕРЕГТИ Зберегти як... @@ -631,7 +603,6 @@ Видалити підмодуль OK Видалити ${0}$... - Злиття ${0}$ в ${1}$... Надіслати ${0}$... Оновити підмодулі Усі підмодулі diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 528596e0f0..d24f14b1e8 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -4,7 +4,6 @@ 关于软件 - 基于代码版本: 关于本软件 发布日期:{0} 浏览版本更新说明 @@ -17,11 +16,6 @@ 填写该工作树的路径。支持相对路径。 分支名 : 选填。默认使用目标文件夹名称。 - 跟踪分支 - 设置上游跟踪分支 - 检出分支方式 : - 创建新分支 - 已有分支 AI助手 模型 重新生成 @@ -30,18 +24,19 @@ 隐藏 SourceGit 隐藏其他 显示全部 - 应用补丁(apply) 尝试三路合并 - 补丁文件 : 选择补丁文件 忽略空白符号 + 补丁来源 : + 文件 + 剪贴板 应用补丁 空白符号处理 : 应用贮藏 在成功应用后丢弃该贮藏 恢复索引中已暂存的变化 已选贮藏 : - 存档(archive) ... + 存档(archive)... 存档文件路径: 选择存档文件的存放路径 指定的提交: @@ -52,47 +47,52 @@ 没有不跟踪更改的文件 加载本地图片 重新加载 - 二进制文件不支持该操作!!! 二分定位(bisect) 终止 标记错误 - 二分定位进行中。当前提交是 '正确' 还是 '错误' ? 标记正确 无法判定 - 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。 + 二分定位进行中。请检出另一个提交并标记其是【正确】还是【错误】 + 二分定位进行中。请标记当前提交为【错误】,或检出另一个提交并标记为【错误】。 + 二分定位进行中。请标记当前的提交是【正确】还是【错误】,然后检出另一个提交。 + 二分定位进行中。当前提交是【正确】还是【错误】? 逐行追溯(blame) 对当前版本的前一版本执行逐行追溯操作 忽略空白符变化 + 按住 Ctrl (Windows及Linux平台) 或 ⌘ (macOS平台) 键并点击提交哈希将基于所选的提交重新计算逐行追溯 选中文件不支持该操作!!! 检出(checkout) ${0}$... + 检出 ${0}$ 为新工作树... 比较选中的 2 个分支 与其他分支或标签比较... 与当前 HEAD 比较 + 与 ${0}$ 比较 复制分支名 - 创建合并请求 ... - 为上游分支 ${0}$ 创建合并请求 ... + 创建合并请求... + 为上游分支 ${0}$ 创建合并请求... 自定义操作 删除 ${0}$... - 删除选中的 {0} 个分支 + 删除选中的 {0} 个分支... 编辑 ${0}$ 的描述... 快进(fast-forward) 到 ${0}$ 拉取(fetch) ${0}$ 至 ${1}$... - GIT工作流 - 完成 ${0}$ + GIT工作流 - 完成 ${0}$... 交互式变基 ${0}$ 到 ${1}$ 合并(merge) ${0}$ 到 ${1}$... - 合并(merge) {0} 个分支到当前分支 - 拉回(pull) ${0}$ + 合并(merge) {0} 个分支到当前分支... + 拉回(pull) ${0}$... 拉回(pull) ${0}$ 内容至 ${1}$... - 推送(push) ${0}$ + 推送(push) ${0}$... 变基(rebase) ${0}$ 至 ${1}$... 重命名 ${0}$... 重置(reset) ${0}$ 到 ${1}$... 切换到 ${0}$ (工作树) - 切换上游分支 ... + 切换上游分支... 领先 {0} 个提交 领先 {0} 个提交,落后 {1} 个提交 落后 {0} 个提交 不存在 + 推送地址 远程 状态 上游分支 @@ -113,18 +113,26 @@ 子模块 : 远程地址 : 检出(checkout)分支 - 检出(checkout)提交 - 提交 : - 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 未提交更改 : 目标分支 : 您当前游离的HEAD包含未被任何分支及标签引用的提交!是否继续? 以下子模块需要更新:{0}是否立即更新? 检出分支并快进 上游分支 : + 检出分支为新的工作树 + 本地分支 : + 新分支名称 : + 选填。默认使用目标文件夹名称。 + 远程分支 : + 新分支跟踪此远程分支 + 工作树路径 : + 必填。可填写相对路径。 从所选贮藏检出分支 新分支 : 所选贮藏 : + 检出提交或标签 + 目标 : + 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 挑选提交 提交信息中追加来源信息 提交列表 : @@ -149,18 +157,19 @@ 分支 & 标签 自定义操作列表 文件列表 - 检出此提交 - 挑选(cherry-pick)此提交 + 检出此提交... + 挑选(cherry-pick)此提交... 挑选(cherry-pick)... 与当前HEAD比较 与本地工作树比较 作者 + 修改时间 提交信息 提交者 + 提交时间 提交指纹 主题 自定义操作 - 丢弃此提交 交互式变基(rebase -i) 丢弃... 编辑... @@ -168,26 +177,23 @@ 交互式变基 ${0}$ 到 ${1}$ 修改提交信息... 合并至父提交... - 合并(merge)此提交至 ${0}$ 合并(merge)... - 推送(push) ${0}$ 到 ${1}$ - 变基(rebase) ${0}$ 到 ${1} - 重置(reset) ${0}$ 到 ${1} - 回滚此提交 - 编辑提交信息 - 另存为补丁 ... - 合并此提交到上一个提交 - 修复至父提交 + 推送(push) ${0}$ 到 ${1}$... + 变基(rebase) ${0}$ 到 ${1}$... + 重置(reset) ${0}$ 到 ${1}$... + 回滚此提交... + 另存为补丁... 变更对比 个文件发生变更 查找变更... + 折叠详细面板 文件列表 LFS文件 查找文件... 子模块 + 请选择查看的文件 基本信息 修改者 - 子提交 提交者 查看包含此提交的分支/标签 本提交已被以下分支/标签包含 @@ -202,15 +208,18 @@ 提交指纹 签名者 : 浏览器中查看 - 列 请输入提交的信息。注意:主题与具体描述中间需要空白行分隔! 主题 比较 - 与当前 HEAD 对比 + 差异文件 + 差异提交 + 左侧独有提交 + 右侧独有提交 + 提示:如果比较的另一方是 HEAD,您可以挑选已选中的提交(使用右键菜单) 仓库配置 提交信息模板 内置变量: - + ${branch_name} 当前分支名 ${files_num} 变更文件数量 ${files} 变更文件路径列表 @@ -222,7 +231,7 @@ 自定义操作 命令行参数 : 内置变量: - + ${REPO} 仓库路径 ${REMOTE} 选中的远程仓库或选中分支所属的远程仓库 ${BRANCH} 选中的分支,对于远程分支不包含远程名 @@ -251,6 +260,7 @@ 分钟 自定义规范化提交类型 默认远程 + 自动更新子模块时启用 '--recursive' 选项 默认合并方式 ISSUE追踪 新增匹配Azure DevOps规则 @@ -308,9 +318,10 @@ 类型: 复制 复制全部文本 + 复制为补丁 复制完整路径 复制路径 - 新建分支 ... + 新建分支... 新分支基于 : 完成后切换到新分支 未提交更改 : @@ -318,7 +329,7 @@ 填写分支名称。 创建本地分支 允许重置已存在的分支 - 新建标签 ... + 新建标签... 标签位于 : 使用GPG签名 标签描述 : @@ -332,6 +343,7 @@ 轻量标签 按住Ctrl键点击将以默认参数运行 剪切 + 仪表板 丢弃更改 不做处理 贮藏并自动恢复 @@ -340,6 +352,7 @@ 子模块 : 删除分支确认 分支名 : + 强制删除,即使包含未合并的提交 您正在删除远程上的分支,请务必小心!!! 同时删除远程分支 ${0}$ 删除多个分支 @@ -361,7 +374,8 @@ 标签名 : 同时删除远程仓库中的此标签 二进制文件 - 文件权限已变化 + 查看新内容 + 空文件 首个差异 忽略空白符号变化 混合对比 @@ -402,9 +416,6 @@ 包括未跟踪的文件 总计{0}项选中更改 本操作不支持回退,请确认后继续!!! - 丢弃提交 - 提交 : - 丢弃后 HEAD : 编辑分支描述 目标 : 书签 : @@ -420,6 +431,9 @@ 不拉取远程标签 远程仓库 : 拉取远程仓库内容 + 二进制文件 + 查看内容 + 定位地址... 不跟踪此文件的更改 自定义操作 放弃更改... @@ -437,14 +451,25 @@ 文件历史 文件变更 文件内容 + 文件类型变更: + 删除: + 目录 + 可执行文件 + 新增文件类型: + 普通文件 + 子模块 + 符号链接 + 未知 GIT工作流 开发分支 : 特性分支 : 特性分支名前缀 : + 完成 ${0}$ 结束特性分支 结束修复分支 结束版本分支 目标分支 : + 合并前执行变基操作 压缩变更为单一提交后合并分支 修复分支 : 修复分支名前缀 : @@ -453,10 +478,12 @@ 发布分支 : 版本分支 : 版本分支名前缀 : + 开始于 : 开始特性分支... 开始特性分支 开始修复分支... 开始修复分支 + 分支名 : 输入分支名 开始版本分支... 开始版本分支 @@ -467,7 +494,7 @@ 规则 : 添加LFS追踪文件规则 拉取LFS对象 (fetch) - 执行`git lfs prune`命令,下载远程LFS对象,但不会更新工作副本。 + 执行`git lfs fetch`命令,下载远程LFS对象,但不会更新工作副本。 拉取LFS对象 启用Git LFS支持 显示LFS对象锁 @@ -491,18 +518,26 @@ 跟踪名为'{0}'的文件 跟踪所有 *{0} 文件 选择前往的提交 - 历史记录 + 高级设置 作者 修改时间 提交时间 - 日期时间 路线图与主题 提交指纹 + 高亮分支 + 全部 + 仅当前分支 + 当前分支和选中的提交 + 仅选中的提交 + 仅选中的提交 (First-parent 模式) 已选中 {0} 项提交 显示列 可以按住 Ctrl 或 Shift 键选择多个提交 可以按住 ⌘ 或 ⇧ 键选择多个提交 小提示: + 以独立窗口中打开 + 提交详情 + 比较 快捷键参考 全局快捷键 克隆远程仓库 @@ -521,23 +556,17 @@ 自动暂存全部变更并提交 新建分支 拉取 (fetch) 远程变更 - 切换左边栏为分支/标签等显示模式(默认) 前往选中提交的子提交 前往选中提交的父提交 打开快捷命令面板 - 切换左边栏为提交搜索模式 拉回 (pull) 远程变更 推送本地变更到远程 重新加载仓库状态 + 展开/折叠提交的详情面板 + 打开/关闭搜索提交面板 显示本地更改 - 显示历史记录 + 显示仪表盘 显示贮藏列表 - 文本编辑器 - 关闭搜索 - 定位到下一个匹配搜索的位置 - 定位到上一个匹配搜索的位置 - 使用外部比对工具查看 - 打开搜索 丢弃 暂存 移出暂存区 @@ -555,6 +584,7 @@ 正在回滚提交 交互式变基 自动贮藏并恢复本地变更 + 绕过 pre-rebase 钩子 起始提交 : 拖拽以便对提交重新排序 目标分支 : @@ -563,6 +593,7 @@ 命令列表 出错了 系统提示 + 检测到新版本 打开其他仓库 页面列表 工作区列表 @@ -571,6 +602,10 @@ 目标分支 : 合并方式 : 合并目标 : + 检测合并冲突中... + 合并操作不会产生冲突 + 检测合并冲突时出现未知错误 + 合并操作将存在冲突文件 先应用 MINE 后 THEIRS 先应用 THEIRS 后 MINE 应用全部 @@ -602,6 +637,8 @@ 打开 系统默认编辑器 浏览应用数据目录 + 缓存数据 + 偏好设置数据 打开文件 使用外部对比工具查看 打开本地仓库 @@ -633,8 +670,12 @@ AI 附加提示词 (请使用 `-` 列出您的要求) API密钥 + 附加 HTTP 请求头部 (格式:每行一条 Name: Value) + 模型 + 自动拉取可用模型列表 配置名称 从环境变量(填写环境变量名)中读取API密钥 + 推理努力等级 服务地址 外观配置 缺省字体 @@ -662,13 +703,13 @@ 在变更列表树中启用紧凑文件夹模式 显示语言 最大历史提交数 - 在提交路线图中显示修改时间而非提交时间 默认显示【本地更改】页 在提交详情页默认打开【变更对比】标签页 - 在提交详情页中显示子提交列表 + 在提交路线图中显示相对时间 在提交路线图中显示标签 SUBJECT字数检测 24小时制 + 在提交路线图中使用精简分支名 生成GitHub风格的默认头像 GIT配置 自动换行转换 @@ -683,6 +724,7 @@ 使用 git-credential-libsecret 替代 git-credential-manager 用户名 默认GIT用户名 + 迁出或合并分支时,默认贮藏并自动恢复本地更改 Git 版本 GPG签名 启用提交签名 @@ -714,6 +756,7 @@ 启用强制推送 本地分支 : 新建 + 绕过 pre-push 钩子 远程仓库 : 修订 : 推送指定修订到远程仓库 @@ -730,21 +773,28 @@ 退出 变基(rebase)操作 自动贮藏并恢复本地变更 + 绕过 pre-rebase 钩子 目标提交 : + 检测变基冲突中... + 未检测到文件冲突 + 检测变基冲突时出现未知错误 + 检测到冲突文件 添加远程仓库 编辑远程仓库 远程名 : 唯一远程名 + 重置推送地址为相同地址 仓库地址 : 远程仓库的地址 复制远程地址 自定义操作 - 删除 ... - 编辑 ... + 删除... + 编辑... 启用自动拉取 拉取(fetch)更新 在浏览器中打开 清理远程已删除分支 + 设置独立推送地址... 移除工作树操作确认 启用`--force`选项 目标工作树 : @@ -765,14 +815,16 @@ 下一步 自定义操作 自定义操作未设置 - 主页 放弃所有更改 在文件浏览器中打开 快速查找分支/标签/子模块 设置在列表中的可见性 + 折叠过滤项列表 不指定 在提交列表中隐藏 + 展开过滤项列表 使用其对提交列表过滤 + 个过滤项已被启用 布局方式 水平排布 竖直排布 @@ -780,11 +832,9 @@ 按提交时间 按拓扑排序 本地分支 - 更多选项... 定位HEAD 新建分支 清空通知列表 - 仅高亮显示当前分支 本仓库(文件夹) 在 {0} 中打开 使用外部工具打开 @@ -793,12 +843,13 @@ 解决冲突 查找提交 作者 - 提交者 变更内容 提交信息 路径 提交指纹 仅在当前分支中查找 + 搜索方式 + 查找提交 仅显示分支、标签所引用的提交 仅显示合并提交中的第一个提交 显示内容 @@ -816,7 +867,6 @@ 按名称 排序 在终端中打开 - 在提交列表中使用相对时间 查看命令日志 访问远程仓库 '{0}' 工作树列表 @@ -834,7 +884,6 @@ 回滚操作确认 目标提交 : 回滚后提交更改 - 编辑提交信息 执行操作中,请耐心等待... 保 存 另存为... @@ -851,6 +900,10 @@ 新版发布时间 : 软件更新 当前已是最新版本。 + 设置远程推送地址 + 推送地址 : + 远程 : + 拉取地址 : 修改子模块追踪分支 子模块 : 当前追踪分支 : @@ -862,11 +915,24 @@ 上游分支 : 复制提交指纹 跳转到提交 - 合并修改至父提交 - 修复至父提交 - 父提交: SSH密钥 : SSH密钥文件 + 打开SSH密钥工具 + 您确定要删除以下SSH密钥吗? + SHA-256 指纹 + 生成SSH密钥 + 再次确认口令: + 邮箱: + 必填项 + 密钥文件名: + 必填项,如:id_github + 输入口令: + 生 成 + 密钥类型: + 为私钥文件设置口令 + 文件路径 + 公钥内容 + SSH密钥工具 开 始 贮藏(stash) 包含未跟踪的文件 @@ -888,9 +954,15 @@ 查看变更 贮藏列表 提交统计 + 提交统计图 总览 本月 本周 + SourceGit 仅会按所选分支最多取出 200000 条提交记录进行分析 + 主要贡献者 + 提交数量 + 百分比 + 提交占比小于1%的作者会被统计入Others分组 贡献者人数: 提交次数: 子模块 @@ -920,6 +992,7 @@ 确 定 创建者 创建时间 + 检出标签... 比较 2 个标签 与其他分支或标签比较... 与当前 HEAD 比较 @@ -932,10 +1005,17 @@ 删除选中 {0} 个标签... 合并 ${0}$ 到 ${1}$... 推送 ${0}$... + 终止运行 + 不受信任的仓库 + 是否执行 `git config --global --add safe.directory {0}` 命令以信任该仓库? + Git 拒绝访问该仓库: + 路径 : + 有 {0} 个仓库因 git 的信任检查被跳过,请手动打开它们并确认是否信任。 更新子模块 更新所有子模块 如未初始化子模块,先初始化 子模块 : + 递归更新子孙模块 更新到子模块自己的远程踪分支 仓库地址 : 日志列表 @@ -962,10 +1042,12 @@ 忽略同目录下所有 *{0} 文件 忽略该目录下的新文件 忽略本文件 + 忽略同目录下所有新文件 修补 现在您已可将其加入暂存区中 清空历史提交信息 您确定要清空所有的历史提交信息记录吗(执行操作后无法撤回)? + 隐藏左侧栏 提交 提交并推送 历史输入/模板 @@ -981,6 +1063,9 @@ 文件冲突已解决 使用 MINE 使用 THEIRS + 放弃所有更改 + 显示左侧栏 + 查找变更... 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 0142bb9b69..97680c6103 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -4,7 +4,6 @@ 關於 - 構建於源代碼修訂版: 關於 SourceGit 發行日期: {0} 版本說明 @@ -12,16 +11,11 @@ 新增忽略檔案 比對模式: 儲存路徑: - 新增工作區 - 工作區路徑: - 填寫該工作區的路徑。支援相對路徑。 + 新增工作樹 + 工作樹路徑: + 填寫該工作樹的路徑。支援相對路徑。 分支名稱: 選填。預設使用目標資料夾名稱。 - 追蹤分支 - 設定遠端追蹤分支 - 簽出分支方式: - 建立新分支 - 已有分支 AI 助理 模型 重新產生 @@ -30,11 +24,12 @@ 隱藏 SourceGit 隱藏其他 顯示全部 - 套用修補檔 (apply patch) 嘗試三向合併 - 修補檔: 選擇修補檔 忽略空白符號 + 修補檔來源: + 檔案 + 剪貼簿 套用修補檔 空白字元處理: 套用擱置變更 @@ -52,52 +47,57 @@ 沒有不追蹤變更的檔案 載入本機圖片... 重新載入 - 二進位檔案不支援該操作! 二分搜尋 (bisect) 中止 標記為錯誤 - 二分搜尋進行中。目前的提交是「良好」是「錯誤」? 標記為良好 無法確認 - 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 + 二分搜尋進行中。請簽出另一個提交,然後標記該提交為「良好」或「錯誤」。 + 二分搜尋進行中。請標記目前的提交為「錯誤」,或簽出另一個提交並標記為「錯誤」。 + 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 + 二分搜尋進行中。目前的提交是「良好」還是「錯誤」? 逐行溯源 (blame) 對上一個版本執行逐行溯源 忽略空白符號變化 + 按住 Ctrl 鍵 (Windows/Linux) 或 ⌘ 鍵 (macOS) 並點擊提交編號,將依據該次提交重新執行逐行溯源 所選擇的檔案不支援該操作! 簽出 (checkout) ${0}$... + 簽出 (checkout) ${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} 次提交 無效 + 推送網址 遠端 狀態 上游分支 遠端網址 - 工作區 + 工作樹 取 消 重設檔案到上一版本 重設檔案為此版本 @@ -113,24 +113,32 @@ 子模組: 遠端網址: 簽出 (checkout) 分支 - 簽出 (checkout) 提交 - 提交: - 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 未提交變更: 目標分支: - 您目前的分離的 HEAD 包含與任何分支/標籤無關的提交! 您要繼續嗎? + 目前的 HEAD 包含未連結至任何分支或標籤的提交! 您是否要繼續? 以下子模組需要更新: {0},您要立即更新嗎? 簽出分支並快轉 上游分支: + 簽出分支為工作樹 + 本機分支: + 新分支名稱: + 選填。預設使用目標資料夾名稱。 + 遠端分支: + 追蹤此遠端分支 + 工作樹路徑: + 填寫該工作樹的路徑。支援相對路徑。 從所選擱置簽出分支 新分支: 所選擱置: + 簽出提交或標籤 + 目標: + 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 揀選提交 提交資訊中追加來源資訊 提交列表: 提交變更 對比的父提交: - 通常您不能對一個合併進行揀選 (cherry-pick),因為您不知道合併的哪一邊應該被視為主線。這個選項指定了作為主線的父提交,允許揀選相對於該提交的修改。 + 通常您無法揀選 (cherry-pick) 合併提交,因為無法判斷哪個父提交應視為主線。此選項可指定作為主線的父提交。 捨棄擱置變更確認 您正在捨棄所有的擱置變更,一經操作便無法復原,是否繼續? 複製 (clone) 遠端存放庫 @@ -149,18 +157,19 @@ 分支 & 標籤 自訂動作 檔案列表 - 簽出 (checkout) 此提交 - 揀選 (cherry-pick) 此提交 + 簽出 (checkout) 此提交... + 揀選 (cherry-pick) 此提交... 揀選 (cherry-pick)... 與目前 HEAD 比較 - 與本機工作區比較 + 與本機工作樹比較 作者 + 修改時間 提交訊息 提交者 + 提交時間 提交編號 標題 自訂動作 - 捨棄此提交 互動式重定基底 (rebase -i) 捨棄... 編輯... @@ -168,26 +177,23 @@ 互動式重定基底 ${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 檔案 搜尋檔案... 子模組 + 請選擇要檢視內容的檔案 基本資訊 作者 - 後續提交 提交者 檢視包含此提交的分支或標籤 本提交包含於以下分支或標籤 @@ -202,11 +208,14 @@ 提交編號 簽署人: 在瀏覽器中檢視 - 欄 請輸入提交訊息,標題與詳細描述之間請使用單行空白區隔。 標題 比較 - 與目前 HEAD 比較 + 差異檔案 + 差異提交 + 左側獨有提交 + 右側獨有提交 + 提示: 如果比較的另一方是 HEAD,您可以揀選 (cherry-pick) 已選擇的提交 (使用滑鼠右鍵) 存放庫設定 提交訊息範本 內建參數: @@ -225,8 +234,8 @@ ${REPO} 存放庫路徑 ${REMOTE} 所選的遠端存放庫或所選分支的遠端 - ${BRANCH} 所選的分支。對於遠端分支,不包含遠端名稱 - ${BRANCH_FRIENDLY_NAME} 所選的分支。對於遠端分支,不包含遠端名稱 + ${BRANCH} 所選的分支。遠端分支不包含遠端名稱 + ${BRANCH_FRIENDLY_NAME} 所選的分支。遠端分支會包含遠端名稱 ${SHA} 所選的提交編號 ${TAG} 所選的標籤 ${FILE} 所選的檔案,相對於存放庫根目錄的路徑 @@ -251,6 +260,7 @@ 分鐘 自訂約定式提交類型 預設遠端存放庫 + 自動更新子模組時啟用「--recursive」選項 預設合併模式 Issue 追蹤 新增符合 Azure DevOps 規則 @@ -276,15 +286,15 @@ 用於本存放庫的使用者名稱 編輯自訂動作輸入控制項 啟用時的指令參數: - 勾選 CheckBox 後,此值將用於指令參數中 + 勾選核取方塊後,此值將用於指令參數中 描述: 預設值: 目標路徑是否為資料夾: 名稱: 選項列表: 請使用英文「|」符號分隔選項 - 字串格式化器: - 可選。用於格式化輸出字串。當輸入為空時將被忽略。請使用 `${VALUE}` 來表示輸入字串。 + 字串格式化: + 選填。用於格式化輸出字串。若輸入為空則會忽略此設定。請使用 `${VALUE}` 來表示輸入字串。 內建變數 ${REPO}、${REMOTE}、${BRANCH}、${BRANCH_FRIENDLY_NAME}、${SHA}、${FILE} 及 ${TAG} 在此處仍可使用 類型: 輸出包含遠端的名稱: @@ -296,7 +306,7 @@ 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)? 暫存全部變更並提交 僅暫存所選變更並提交 - 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存變更並提交? + 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或自動暫存變更並提交? 系統提示 您需要重新啟動此應用程式才能套用變更! 產生約定式提交訊息 @@ -308,6 +318,7 @@ 類型: 複製 複製全部內容 + 複製為修補檔 複製完整路徑 複製路徑 新增分支... @@ -332,14 +343,16 @@ 輕量標籤 按住 Ctrl 鍵將直接以預設參數執行 剪下 + 儀表板 捨棄變更 不做處理 擱置變更並自動復原 取消初始化子模組 強制取消,即使包含本機變更 - 子模組: + 子模組: 刪除分支確認 分支名稱: + 強制刪除,即使包含未合併的提交 您正在刪除遠端上的分支,請務必小心! 同時刪除遠端分支 ${0}$ 刪除多個分支 @@ -361,7 +374,8 @@ 標籤名稱: 同時刪除遠端存放庫中的此標籤 二進位檔案 - 檔案權限已變更 + 檢視新內容 + 空檔案 第一個差異 忽略空白符號變化 混合對比 @@ -390,7 +404,7 @@ 減少可見的行數 增加可見的行數 請選擇需要對比的檔案 - 目錄内容變更歷史 + 目錄內容變更歷史 未提交的本機變更 目前分支 HEAD 與上游不相符 已更新至最新 @@ -402,9 +416,6 @@ 包含未追蹤檔案 將捨棄總計 {0} 項已選取的變更 您無法復原此操作,請確認後再繼續! - 捨棄提交 - 提交: - 捨棄後新的 HEAD: 編輯分支的描述 目標: 書籤: @@ -420,6 +431,9 @@ 不提取遠端標籤 遠端存放庫: 提取遠端存放庫內容 + 二進位檔案 + 檢視內容 + 前往地址... 不追蹤此檔案的變更 自訂動作 捨棄變更... @@ -436,15 +450,26 @@ 使用對方版本 (theirs) 檔案歷史 檔案變更 - 檔案内容 + 檔案內容 + 檔案類型變更: + 刪除檔案類型: + 目錄 + 可執行檔案 + 新增檔案類型: + 一般檔案 + 子模組 + 符號連結 + 未知 Git 工作流 開發分支: 功能分支: 功能分支前置詞: + 完成 ${0}$ 完成功能分支 完成修復分支 完成發行分支 目標分支: + 合併前重定基底 壓縮為單一提交後合併 修復分支: 修復分支前置詞: @@ -453,10 +478,12 @@ 發行分支: 版本分支: 發行分支前置詞: + 開始於: 開始功能分支... 開始功能分支 開始修復分支... 開始修復分支 + 分支名稱: 輸入分支名稱 開始發行分支... 開始發行分支 @@ -491,18 +518,26 @@ 追蹤名稱為「{0}」的檔案 追蹤所有 *{0} 檔案 選取要前往的提交 - 歷史記錄 + 進階選項 作者 修改時間 提交時間 - 日期時間 路線圖與訊息標題 提交編號 + 上色分支 + 全部 + 僅目前分支 + 目前分支和選取的提交 + 僅選取的提交 + 僅選取的提交 (First-parent 模式) 已選取 {0} 項提交 顯示欄位 可以按住 Ctrl 或 Shift 鍵選擇多個提交 可以按住 ⌘ 或 ⇧ 鍵選擇多個提交 小提示: + 在新視窗中開啟 + 提交詳細資訊 + 比較 快速鍵參考 全域快速鍵 複製 (clone) 遠端存放庫 @@ -521,28 +556,22 @@ 自動暫存全部變更並提交 新增分支 提取 (fetch) 遠端的變更 - 切換左邊欄為分支/標籤等顯示模式 (預設) 前往所選提交的子提交 前往所選提交的父提交 開啟命令面板 - 切換左邊欄為歷史搜尋模式 拉取 (pull) 遠端的變更 推送 (push) 本機變更到遠端存放庫 強制重新載入存放庫 + 展開/收合提交詳細面板 + 開啟/關閉搜尋提交面板 顯示本機變更 - 顯示歷史記錄 - 顯示擱置變更列表 - 文字編輯器快速鍵 - 關閉搜尋面板 - 前往下一個搜尋相符的位置 - 前往上一個搜尋相符的位置 - 使用外部比對工具檢視 - 開啟搜尋面板 + 顯示儀表板 + 顯示擱置變更 捨棄 暫存 取消暫存 初始化存放庫 - 您是否要在該路徑執行 git init 以進行初始化? + 您是否要在該路徑執行 `git init` 以初始化存放庫? 無法在指定路徑開啟本機存放庫。原因: 路徑: 揀選 (cherry-pick) 操作進行中。 @@ -555,6 +584,7 @@ 正在復原提交 互動式重定基底 自動擱置變更並復原本機變更 + 繞過 pre-rebase hook 起始提交: 拖曳以重新排序提交 目標分支: @@ -563,6 +593,7 @@ 命令列表 發生錯誤 系統提示 + 有新版本可用 開啟存放庫 頁面列表 工作區列表 @@ -571,6 +602,10 @@ 目標分支: 合併方式: 合併來源: + 正在偵測合併衝突... + 合併操作不會產生衝突 + 偵測合併衝突時發生未知錯誤 + 合併操作將產生檔案衝突 先套用我方版本 (ours),再套用對方版本 (theirs) 先套用對方版本 (theirs),再套用我方版本 (ours) 套用兩者 @@ -602,6 +637,8 @@ 開啟 系統預設編輯器 瀏覽程式資料目錄 + 快取資料 + 偏好設定資料 開啟檔案 使用外部比對工具檢視 開啟本機存放庫 @@ -633,8 +670,12 @@ AI 附加提示詞 (請使用 '-' 列出您的要求) API 金鑰 + 額外標頭 (每行一組 `名稱: 值`) + 模型 + 自動取得可用的模型列表 名稱 從環境變數中 (輸入環境變數名稱) 讀取 API 金鑰 + 推理努力等級 伺服器 外觀設定 預設字型 @@ -662,20 +703,20 @@ 在樹狀變更目錄中啟用密集資料夾模式 顯示語言 最大歷史提交數 - 在提交路線圖中顯示修改時間而非提交時間 預設顯示 [本機變更] 頁面 在提交詳細資訊頁面預設顯示 [變更對比] - 在提交詳細資訊中顯示後續提交 + 在提交路線圖中顯示相對時間 在路線圖中顯示標籤 提交標題字數偵測 24 小時制 + 在提交路線圖中使用簡潔的分支標籤 產生 GitHub 風格的預設頭貼 Git 設定 自動換行轉換 預設複製 (clone) 路徑 電子郵件 預設 Git 使用者電子郵件 - 拉取變更時進行清理 + 提取後清理遠端已刪除分支 對比檔案時,預設忽略行末的 CR 變更 本軟體要求 Git 最低版本為 2.25.1 安裝路徑 @@ -683,6 +724,7 @@ 使用 git-credential-libsecret 取代 git-credential-manager 使用者名稱 預設 Git 使用者名稱 + 在簽出或合併分支時,預設擱置變更並自動復原 Git 版本 GPG 簽章 啟用提交簽章 @@ -695,13 +737,13 @@ 第三方工具整合 終端機/Shell 啟動參數 - 請使用「.」標示當前工作目錄 + 請使用「.」表示目前工作目錄 安裝路徑 終端機/Shell 清理遠端已刪除分支 目標: - 清理工作區 - 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作區資訊 + 清理工作樹 + 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作樹資訊 拉取 (pull) 拉取分支: 本機分支: @@ -714,6 +756,7 @@ 啟用強制推送 本機分支: 新增 + 繞過 pre-push hook 遠端存放庫: 修訂: 推送修訂到遠端存放庫 @@ -730,11 +773,17 @@ 結束 重定基底 (rebase) 操作 自動擱置變更並復原本機變更 + 繞過 pre-rebase hook 目標提交: + 正在偵測重定基底衝突... + 重定基底操作不會產生衝突 + 偵測重定基底衝突時發生未知錯誤 + 重定基底操作將產生檔案衝突 新增遠端存放庫 編輯遠端存放庫 遠端名稱: 唯一遠端名稱 + 重設推送網址為存放庫網址 存放庫網址: 遠端存放庫的網址 複製遠端網址 @@ -743,20 +792,21 @@ 編輯... 啟用定時自動提取 提取 (fetch) 更新 - 在瀏覽器中存取網址 + 在瀏覽器中開啟網址 清理遠端已刪除分支 - 刪除工作區操作確認 + 設定推送網址... + 刪除工作樹操作確認 啟用 [--force] 選項 - 目標工作區: + 目標工作樹: 分支重新命名 新名稱: 新的分支名稱不能與現有分支名稱相同 分支: 中止 - 自動提取遠端變更中... + 正在自動提取遠端變更... 排序 依建立時間 - 依名稱升序 + 依名稱遞增排序 清理本存放庫 (GC) 本操作將執行 `git gc` 指令。 清空篩選規則 @@ -765,14 +815,16 @@ 下一步 自訂動作 沒有自訂的動作 - 首頁 捨棄所有變更 在檔案瀏覽器中開啟 - 快速搜尋分支/標籤/子模組 + 篩選分支/標籤/子模組 篩選以顯示或隱藏 + 摺疊篩選器 取消指定 在提交列表中隱藏 + 展開篩選器 以其篩選提交列表 + 個篩選規則已啟用 版面配置 橫向顯示 縱向顯示 @@ -780,11 +832,9 @@ 依時間排序 依拓撲排序 本機分支 - 更多選項... 回到 HEAD 新增分支 清除所有通知 - 路線圖中僅對目前分支上色 此存放庫 (資料夾) 在 {0} 中開啟 使用外部工具開啟 @@ -793,18 +843,19 @@ 解決衝突 搜尋提交 作者 - 提交者 變更內容 提交訊息 路徑 提交編號 僅搜尋目前分支 + 搜尋方式 + 搜尋提交 只顯示分支和標籤引用的提交 只顯示合併提交的第一父提交 顯示內容 顯示失去參照的提交 - 以樹型結構展示 - 以樹型結構展示 + 以樹狀結構顯示 + 以樹狀結構顯示 跳過此提交 提交統計 子模組列表 @@ -816,11 +867,10 @@ 依名稱 排序 在終端機中開啟 - 在提交列表中使用相對時間 檢視 Git 指令記錄 檢視遠端存放庫 '{0}' - 工作區列表 - 新增工作區 + 工作樹列表 + 新增工作樹 清理 遠端存放庫網址 重設目前分支到指定版本 @@ -834,8 +884,7 @@ 復原操作確認 目標提交: 復原後提交變更 - 編輯提交訊息 - 執行操作中,請耐心等待... + 正在執行操作,請耐心等待... 儲存 另存新檔... 修補檔已成功儲存! @@ -851,6 +900,10 @@ 新版本發行日期: 軟體更新 目前已是最新版本。 + 設定遠端存放庫的推送網址 + 推送網址: + 遠端: + 網址: 設定子模組的追蹤分支 子模組: 目前追蹤分支: @@ -862,11 +915,24 @@ 上游分支: 複製提交編號 前往此提交 - 合併變更入父提交 - 修正至父提交 - 父提交: SSH 金鑰: SSH 金鑰檔案 + 開啟 SSH 金鑰工具 + 您確定要刪除以下的金鑰嗎? + 指紋 + 產生 SSH 金鑰 + 再次輸入您的密碼: + 電子郵件: + 必填。 + 金鑰名稱: + 必填,例如: id_github + 輸入您的密碼: + 產 生 + 類型: + 為私密金鑰檔案設定密碼 + 路徑 + 公開金鑰 + SSH 金鑰工具 開 始 擱置變更 (stash) 包含未追蹤的檔案 @@ -888,9 +954,15 @@ 檢視變更 擱置變更列表 提交統計 + 提交統計圖 總覽 本月 本週 + SourceGit 僅會依所選分支取出最多 200,000 筆提交記錄進行分析 + 主要貢獻者 + 提交次數 + 百分比 + 提交佔比低於 1% 的貢獻者會被歸入 Others 分組 貢獻者人數: 提交次數: 子模組 @@ -906,8 +978,8 @@ 相對存放庫路徑: 本機存放的相對路徑。 刪除 - 更改追蹤分支 - 更改遠端網址 + 變更追蹤分支 + 變更遠端網址 狀態 未提交變更 未初始化 @@ -920,8 +992,9 @@ 確 定 建立者 建立時間 + 簽出標籤... 比較 2 個標籤 - 與分支或標籤進行比較... + 與其他分支或標籤比較... 與目前 HEAD 比較 標籤訊息 標籤名稱 @@ -932,10 +1005,17 @@ 刪除所選的 {0} 個標籤... 合併 ${0}$ 到 ${1}$... 推送 ${0}$... + 終止執行 + 不受信任的存放庫 + 您是否要執行 `git config --global --add safe.directory {0}` 命令以信任該存放庫? + Git 拒絕存取該存放庫: + 路徑: + 有 {0} 個存放庫因 git 的信任檢查而被略過,請手動開啟並確認是否信任。 更新子模組 更新所有子模組 如果子模組尚未初始化,則將其初始化 子模組: + 遞迴更新子模組 更新至子模組的遠端追蹤分支 存放庫網址: 記錄 @@ -951,28 +1031,30 @@ 支援拖放以新增目錄與自訂群組。 編輯 調整存放庫分組 - 開啟所有包含存放庫 + 開啟所有存放庫 開啟本機存放庫 開啟終端機 重新掃描預設複製 (clone) 目錄下的存放庫 - 快速搜尋存放庫... + 篩選存放庫... 本機變更 加入至 .gitignore 忽略清單 忽略所有 *{0} 檔案 忽略同路徑下所有 *{0} 檔案 忽略本路徑下的新增檔案 忽略本檔案 + 忽略同路徑下所有新增檔案 修補 現在您已可將其加入暫存區中 - 清除提交訊息歷史 + 清除提交訊息記錄 您確定要清除所有提交訊息記錄嗎 (執行後無法復原)? + 收合側邊欄 提 交 提交並推送 - 歷史輸入/範本 + 提交訊息記錄/範本 觸發點擊事件 提交 (修改原始提交) 自動暫存全部變更並提交 - 您正在向一个分離狀態的 HEAD 提交變更,您確定要繼續提交嗎? + HEAD 目前處於分離狀態。您確定要繼續提交變更嗎? 您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎? 偵測到衝突 解決衝突 @@ -981,6 +1063,9 @@ 檔案衝突已解決 使用我方版本 (ours) 使用對方版本 (theirs) + 捨棄所有變更 + 展開側邊欄 + 篩選變更... 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 @@ -997,14 +1082,14 @@ 範本: ${0}$ 工作區: 設定工作區... - 本機工作區 + 本機工作樹 分支 - 複製工作區路徑 + 複製工作樹路徑 最新提交 - 鎖定工作區 - 開啟工作區 + 鎖定工作樹 + 開啟工作樹 位置 - 移除工作區 - 解除鎖定工作區 + 移除工作樹 + 解除鎖定工作樹 是 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 0c4dbb7fb7..d1ba76c821 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -4,7 +4,6 @@ xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" xmlns:ae="using:AvaloniaEdit" - xmlns:aec="using:AvaloniaEdit.CodeCompletion" xmlns:aee="using:AvaloniaEdit.Editing" xmlns:aes="using:AvaloniaEdit.Search"> @@ -17,12 +16,13 @@ Red - + - + + @@ -38,7 +38,7 @@ - + - + - + - - + - + @@ -709,25 +712,6 @@ - - - - - - + + + + + + + + - - - - + + + + + + - - - + @@ -1385,13 +1424,19 @@ - + - - - - + diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index 02f1802434..36b97c8984 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -2,12 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - #FFF0F5F9 + #FFF4F4F4 #FF999999 - #FFCFDEEA - #FFF0F5F9 - #FFF8F8F8 - #FFFAFAFA + #FFF2F0F0 + #FFF0F3F5 + #FFFAFAFA + #FFFFFFFF + #FFFFFFFF #FFB0CEE8 #FF1F1F1F #FF836C2E @@ -31,7 +32,7 @@ #0000EE #FFE4E4E4 Black - #FFF0F5F9 + #FFFBFBFB @@ -40,9 +41,10 @@ #FF1F1F1F #FF2F2F2F #FF2B2B2B + #FF393939 #FF1C1C1C #FF8F8F8F - #FFDDDDDD + #FFE0E0E0 #FFFAFAD2 #FF252525 #400078D7 @@ -53,8 +55,8 @@ #FF303030 #FF333333 #FF4F4F4F - #FFDDDDDD - #40F1F1F1 + #FFDFDFDF + #FF9F9F9F #3C000000 #C03A5C3F #C0633F3E @@ -64,7 +66,7 @@ #4DAAFC #FF383838 #FFF0F0F0 - #FF2B2B2B + #FF272727 @@ -73,6 +75,7 @@ + @@ -99,8 +102,5 @@ - - - fonts:Inter#Inter - fonts:SourceGit#JetBrains Mono + diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 4b55be7aa1..65f49a3a40 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 @@ -47,16 +48,15 @@ - + + + + + - - - - - @@ -68,4 +68,16 @@ + + + + + + + + + + + + diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index 4f54678c2f..e9e4692feb 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -49,6 +49,9 @@ public AIAssistant(Repository repo, AI.Service service, List chan foreach (var c in changes) SerializeChange(c, builder); _changeList = builder.ToString(); + + if (changes.Count > 0 && changes[0].DataForAmend is { } amend) + _amendParent = amend.ParentSHA; } public async Task GenAsync() @@ -63,6 +66,7 @@ public async Task GenAsync() var responseBuilder = new StringBuilder(); var foundResponse = false; + var currentBranchName = _repo.CurrentBranch?.Name ?? "main"; Text = builder.ToString(); Response = string.Empty; @@ -70,7 +74,7 @@ public async Task GenAsync() try { - await agent.GenerateCommitMessageAsync(_repo.FullPath, _changeList, message => + await agent.GenerateCommitMessageAsync(_repo.FullPath, currentBranchName, _changeList, _amendParent, message => { builder.AppendLine(message); @@ -144,6 +148,7 @@ private void SerializeChange(Models.Change c, StringBuilder builder) private readonly Repository _repo = 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; diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index fb5f0264bc..e16f195d53 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; @@ -101,13 +107,19 @@ public override async Task Sure() if (succ) { + var remote = new Models.Remote() + { + Name = _name, + PrivateSSHKey = _useSSH ? _sshkey : null, + }; + await new Commands.Config(_repo.FullPath) .Use(log) .SetAsync($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); - await new Commands.Fetch(_repo.FullPath, _name, false, false) + await new Commands.Fetch(_repo.FullPath, remote, FetchWithoutTags, false) .Use(log) - .RunAsync(); + .ExecAsync(); } log.Complete(); diff --git a/src/ViewModels/AddToIgnore.cs b/src/ViewModels/AddToIgnore.cs index a502c1725c..c88cf1e7fc 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 a818fdc180..4d5eb9f1e2 100644 --- a/src/ViewModels/AddWorktree.cs +++ b/src/ViewModels/AddWorktree.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -8,87 +7,35 @@ namespace SourceGit.ViewModels { public class AddWorktree : Popup { - [Required(ErrorMessage = "Worktree path is required!")] [CustomValidation(typeof(AddWorktree), nameof(ValidateWorktreePath))] - public string Path + public string WorktreePath { - get => _path; - set => SetProperty(ref _path, value, true); + get => _worktreePath; + set => SetProperty(ref _worktreePath, value, true); } - public bool CreateNewBranch + [CustomValidation(typeof(AddWorktree), nameof(ValidateBranchName))] + public string BranchName { - get => _createNewBranch; - set - { - if (SetProperty(ref _createNewBranch, value, true)) - { - if (value) - SelectedBranch = string.Empty; - else - SelectedBranch = LocalBranches.Count > 0 ? LocalBranches[0] : string.Empty; - } - } - } - - public List LocalBranches - { - get; - private set; - } - - public string SelectedBranch - { - get => _selectedBranch; - set => SetProperty(ref _selectedBranch, value); - } - - public bool SetTrackingBranch - { - get => _setTrackingBranch; - set - { - if (SetProperty(ref _setTrackingBranch, value)) - AutoSelectTrackingBranch(); - } - } - - public List RemoteBranches - { - get; - private set; - } - - public Models.Branch SelectedTrackingBranch - { - get => _selectedTrackingBranch; - set => SetProperty(ref _selectedTrackingBranch, value); + get => _branchName; + set => SetProperty(ref _branchName, value, true); } public AddWorktree(Repository repo) { _repo = repo; - - LocalBranches = new List(); - RemoteBranches = new List(); - foreach (var branch in repo.Branches) - { - if (branch.IsLocal) - LocalBranches.Add(branch.Name); - else - RemoteBranches.Add(branch); - } + _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), "new-worktree"); } public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) { if (ctx.ObjectInstance is not AddWorktree creator) - return new ValidationResult("Missing runtime context to create branch!"); + return new ValidationResult("Missing runtime context to add worktree!"); if (string.IsNullOrEmpty(path)) return new ValidationResult("Worktree path is required!"); - var fullPath = System.IO.Path.IsPathRooted(path) ? path : System.IO.Path.Combine(creator._repo.FullPath, path); + var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path); var info = new DirectoryInfo(fullPath); if (info.Exists) { @@ -104,43 +51,42 @@ public static ValidationResult ValidateWorktreePath(string path, ValidationConte return ValidationResult.Success; } + public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) + { + if (ctx.ObjectInstance is not AddWorktree creator) + return new ValidationResult("Missing runtime context to add worktree!"); + + var test = !string.IsNullOrEmpty(name) ? name : Path.GetFileName(creator._worktreePath.TrimEnd('/').TrimEnd('\\')); + if (!Models.RefName.IsValidBranchName(test)) + return new ValidationResult($"{test} is not a valid branch name!"); + + foreach (var b in creator._repo.Branches) + { + if (b.IsLocal && b.Name.Equals(test, StringComparison.Ordinal)) + return new ValidationResult($"A branch with name '{test}' already exists!"); + } + + return ValidationResult.Success; + } + public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = "Adding worktree ..."; + ProgressDescription = $"Adding a new worktree ..."; - var branchName = _selectedBranch; - var tracking = (_setTrackingBranch && _selectedTrackingBranch != null) ? _selectedTrackingBranch.FriendlyName : string.Empty; var log = _repo.CreateLog("Add Worktree"); - Use(log); var succ = await new Commands.Worktree(_repo.FullPath) .Use(log) - .AddAsync(_path, branchName, _createNewBranch, tracking); + .AddAsync(_worktreePath, _branchName); log.Complete(); return succ; } - private void AutoSelectTrackingBranch() - { - if (!_setTrackingBranch || RemoteBranches.Count == 0) - return; - - var name = string.IsNullOrEmpty(_selectedBranch) ? System.IO.Path.GetFileName(_path.TrimEnd('/', '\\')) : _selectedBranch; - var remoteBranch = RemoteBranches.Find(b => b.Name.EndsWith(name, StringComparison.Ordinal)); - if (remoteBranch == null) - remoteBranch = RemoteBranches[0]; - - SelectedTrackingBranch = remoteBranch; - } - - private Repository _repo = null; - private string _path = string.Empty; - private bool _createNewBranch = true; - private string _selectedBranch = string.Empty; - private bool _setTrackingBranch = false; - private Models.Branch _selectedTrackingBranch = null; + private readonly Repository _repo = null; + private string _worktreePath = string.Empty; + private string _branchName = string.Empty; } } diff --git a/src/ViewModels/Apply.cs b/src/ViewModels/Apply.cs index 3578c12f27..14ecc07303 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,25 +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() @@ -52,20 +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 extra = ThreeWayMerge ? "--3way" : string.Empty; - var succ = await new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, extra) + 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/BinaryFileViewer.cs b/src/ViewModels/BinaryFileViewer.cs new file mode 100644 index 0000000000..076887970d --- /dev/null +++ b/src/ViewModels/BinaryFileViewer.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class BinaryFileViewer : ObservableObject + { + public string File + { + get => _file; + } + + public bool IsLoading + { + get => _isLoading; + private set => SetProperty(ref _isLoading, value); + } + + public Models.BinaryFile Content + { + get => _content; + private set => SetProperty(ref _content, value); + } + + public BinaryFileViewer(string repo, string file, string revision) + { + _repo = repo; + _file = file; + _revision = revision; + } + + public async Task LoadAsync() + { + if (_revision != null) + { + string saveTo = Path.GetTempFileName(); + await Commands.SaveRevisionFile.RunAsync(_repo, _revision, _file, saveTo); + + Content = new Models.BinaryFile(saveTo, true); + } + else + { + Content = new Models.BinaryFile(Path.Combine(_repo, _file), false); + } + + IsLoading = false; + } + + public void Cleanup() + { + _repo = null; + _file = null; + _revision = null; + + _content?.Dispose(); + _content = null; + } + + private bool _isLoading = true; + private string _repo = null; + private string _file = null; + private string _revision = null; + private Models.BinaryFile _content = null; + } +} diff --git a/src/ViewModels/Blame.cs b/src/ViewModels/Blame.cs index be05fccd40..c11e4c673f 100644 --- a/src/ViewModels/Blame.cs +++ b/src/ViewModels/Blame.cs @@ -106,10 +106,10 @@ public void Forward() public void GotoPrevRevision() { if (_prevRevision != null) - NavigateToCommit(_file, _prevRevision.SHA.Substring(0, 10)); + NavigateToCommit(_file, _prevRevision.SHA.Substring(0, 10), true); } - public void NavigateToCommit(string file, string sha) + public void NavigateToCommit(string file, string sha, bool changeBlame) { if (App.GetLauncher() is { Pages: { } pages }) { @@ -123,7 +123,7 @@ public void NavigateToCommit(string file, string sha) } } - if (Revision.SHA.StartsWith(sha, StringComparison.Ordinal)) + if (!changeBlame || Revision.SHA.StartsWith(sha, StringComparison.Ordinal)) return; var count = _navigationHistory.Count; diff --git a/src/ViewModels/BlockNavigation.cs b/src/ViewModels/BlockNavigation.cs index 0debfba0ec..406e283453 100644 --- a/src/ViewModels/BlockNavigation.cs +++ b/src/ViewModels/BlockNavigation.cs @@ -123,13 +123,27 @@ public void UpdateByChunk(TextDiffSelectedChunk chunk) if (chunkStart > block.End) continue; - if (chunkEnd < block.Start) + var hasChangeAfter = false; + for (var j = i; j < _blocks.Count; j++) { - _current = i - 1; - break; + var test = _blocks[j]; + if (chunkEnd < test.End) + { + _current = i; + hasChangeAfter = true; + break; + } } - _current = i; + if (!hasChangeAfter) + { + if (chunkStart > block.Start) + _current = i; + else + _current = i - 1; + } + + break; } } diff --git a/src/ViewModels/ChangeSelection.cs b/src/ViewModels/ChangeSelection.cs new file mode 100644 index 0000000000..2e2f8f3388 --- /dev/null +++ b/src/ViewModels/ChangeSelection.cs @@ -0,0 +1,82 @@ +using System.Collections; +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public class ChangeSelection + { + public List Changes { get; } + public bool IsSingleFolder { get; } + public string SingleFolderPath { get; } + public bool HasFolder { get; } + + public int Count => Changes.Count; + + public ChangeSelection(IList selected) + { + var changes = new List(); + + IsSingleFolder = false; + SingleFolderPath = null; + HasFolder = false; + + if (selected is { Count: > 0 }) + { + foreach (var item in selected) + { + if (item is Models.Change c) + { + changes.Add(c); + } + else if (item is ChangeTreeNode node) + { + CollectChangesInNode(changes, node); + + if (node.IsFolder && !HasFolder) + HasFolder = true; + } + } + + if (selected.Count == 1 && selected[0] is ChangeTreeNode { IsFolder: true } folder) + { + IsSingleFolder = true; + SingleFolderPath = folder.FullPath; + } + } + + Changes = changes; + } + + public bool IsChanged(ChangeSelection other) + { + if (other == null || + IsSingleFolder != other.IsSingleFolder || + SingleFolderPath != other.SingleFolderPath || + HasFolder != other.HasFolder || + Changes.Count != other.Changes.Count) + return true; + + foreach (var c in other.Changes) + { + if (!Changes.Contains(c)) + return true; + } + + return false; + } + + private static void CollectChangesInNode(List outs, ChangeTreeNode node) + { + if (node.IsFolder) + { + foreach (var child in node.Children) + CollectChangesInNode(outs, child); + } + else + { + if (!outs.Contains(node.Change)) + outs.Add(node.Change); + } + } + } +} diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 9cdbdc161a..3c47d47a37 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -24,7 +24,10 @@ public Checkout(Repository repo, Models.Branch branch) { _repo = repo; _branch = branch; - DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 13d9e922a0..50dc99aa39 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -30,7 +30,10 @@ public CheckoutAndFastForward(Repository repo, Models.Branch localBranch, Models _repo = repo; LocalBranch = localBranch; RemoteBranch = remoteBranch; - DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() diff --git a/src/ViewModels/CheckoutAsWorktree.cs b/src/ViewModels/CheckoutAsWorktree.cs new file mode 100644 index 0000000000..2963db85f0 --- /dev/null +++ b/src/ViewModels/CheckoutAsWorktree.cs @@ -0,0 +1,72 @@ +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutAsWorktree : Popup + { + public Models.Branch Branch + { + get => _branch; + } + + [CustomValidation(typeof(CheckoutAsWorktree), nameof(ValidateWorktreePath))] + public string WorktreePath + { + get => _worktreePath; + set => SetProperty(ref _worktreePath, value, true); + } + + public CheckoutAsWorktree(Repository repo, Models.Branch branch) + { + _repo = repo; + _branch = branch; + _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), branch.Name.Replace('/', '-').Replace('\\', '-')); + } + + public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) + { + if (ctx.ObjectInstance is not CheckoutAsWorktree creator) + return new ValidationResult("Missing runtime context to checkout branch as worktree!"); + + if (string.IsNullOrEmpty(path)) + return new ValidationResult("Worktree path is required!"); + + var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path); + var info = new DirectoryInfo(fullPath); + if (info.Exists) + { + var files = info.GetFiles(); + if (files.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + + var folders = info.GetDirectories(); + if (folders.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + } + + return ValidationResult.Success; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Checkout branch '{_branch.Name}' as worktree ..."; + + var log = _repo.CreateLog("Add Worktree"); + Use(log); + + var succ = await new Commands.Worktree(_repo.FullPath) + .Use(log) + .AddWithLocalBranchAsync(_worktreePath, _branch.Name); + + log.Complete(); + return succ; + } + + private readonly Repository _repo; + private readonly Models.Branch _branch; + private string _worktreePath; + } +} diff --git a/src/ViewModels/CheckoutBranchFromStash.cs b/src/ViewModels/CheckoutBranchFromStash.cs index d62f4f2dff..a87561805d 100644 --- a/src/ViewModels/CheckoutBranchFromStash.cs +++ b/src/ViewModels/CheckoutBranchFromStash.cs @@ -12,7 +12,6 @@ public Models.Stash Target } [Required(ErrorMessage = "Branch name is required!")] - [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(CheckoutBranchFromStash), nameof(ValidateBranchName))] public string BranchName { @@ -30,6 +29,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is CheckoutBranchFromStash caller) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + foreach (var b in caller._repo.Branches) { if (b.FriendlyName.Equals(name, StringComparison.Ordinal)) diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutDetached.cs similarity index 71% rename from src/ViewModels/CheckoutCommit.cs rename to src/ViewModels/CheckoutDetached.cs index 9f74985ee9..4e4d0def07 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutDetached.cs @@ -2,9 +2,9 @@ namespace SourceGit.ViewModels { - public class CheckoutCommit : Popup + public class CheckoutDetached : Popup { - public Models.Commit Commit + public object Target { get; } @@ -20,17 +20,32 @@ public Models.DealWithLocalChanges DealWithLocalChanges set; } - public CheckoutCommit(Repository repo, Models.Commit commit) + public CheckoutDetached(Repository repo, Models.Commit commit) { _repo = repo; - Commit = commit; - DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + _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 '{Commit.SHA}' ..."; + ProgressDescription = $"Checkout Commit '{_revision}' ..."; var log = _repo.CreateLog("Checkout Commit"); Use(log); @@ -54,7 +69,7 @@ public override async Task Sure() { succ = await new Commands.Checkout(_repo.FullPath) .Use(log) - .CommitAsync(Commit.SHA, false); + .CommitAsync(_revision, false); } else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { @@ -76,13 +91,13 @@ public override async Task Sure() succ = await new Commands.Checkout(_repo.FullPath) .Use(log) - .CommitAsync(Commit.SHA, false); + .CommitAsync(_revision, false); } else { succ = await new Commands.Checkout(_repo.FullPath) .Use(log) - .CommitAsync(Commit.SHA, true); + .CommitAsync(_revision, true); } if (succ) @@ -100,5 +115,6 @@ public override async Task Sure() } private readonly Repository _repo = null; + private readonly string _revision = string.Empty; } } diff --git a/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs b/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs new file mode 100644 index 0000000000..a054a30134 --- /dev/null +++ b/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs @@ -0,0 +1,108 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutRemoteBranchAsWorktree : Popup + { + public Models.Branch RemoteBranch + { + get => _remoteBranch; + } + + [CustomValidation(typeof(CheckoutRemoteBranchAsWorktree), nameof(ValidateWorktreePath))] + public string WorktreePath + { + get => _worktreePath; + set => SetProperty(ref _worktreePath, value, true); + } + + [CustomValidation(typeof(CheckoutRemoteBranchAsWorktree), nameof(ValidateBranchName))] + public string BranchName + { + get => _branchName; + set => SetProperty(ref _branchName, value, true); + } + + public bool Tracking + { + get => _tracking; + set => SetProperty(ref _tracking, value); + } + + public CheckoutRemoteBranchAsWorktree(Repository repo, Models.Branch remoteBranch) + { + _repo = repo; + _remoteBranch = remoteBranch; + _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), remoteBranch.Name.Replace('/', '-').Replace('\\', '-')); + _branchName = remoteBranch.Name; + _tracking = true; + } + + public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) + { + if (ctx.ObjectInstance is not CheckoutRemoteBranchAsWorktree creator) + return new ValidationResult("Missing runtime context to checkout branch as worktree!"); + + if (string.IsNullOrEmpty(path)) + return new ValidationResult("Worktree path is required!"); + + var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path); + var info = new DirectoryInfo(fullPath); + if (info.Exists) + { + var files = info.GetFiles(); + if (files.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + + var folders = info.GetDirectories(); + if (folders.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + } + + return ValidationResult.Success; + } + + public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) + { + if (ctx.ObjectInstance is not CheckoutRemoteBranchAsWorktree creator) + return new ValidationResult("Missing runtime context to checkout branch as worktree!"); + + var test = !string.IsNullOrEmpty(name) ? name : Path.GetFileName(creator._worktreePath.TrimEnd('/').TrimEnd('\\')); + if (!Models.RefName.IsValidBranchName(test)) + return new ValidationResult($"{test} is not a valid branch name!"); + + foreach (var b in creator._repo.Branches) + { + if (b.IsLocal && b.Name.Equals(test, StringComparison.Ordinal)) + return new ValidationResult($"A branch with name '{test}' already exists!"); + } + + return ValidationResult.Success; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Checkout branch '{_remoteBranch.FriendlyName}' as worktree ..."; + + var log = _repo.CreateLog("Add Worktree"); + Use(log); + + var succ = await new Commands.Worktree(_repo.FullPath) + .Use(log) + .AddWithRemoteBranchAsync(_worktreePath, _branchName, _remoteBranch.FriendlyName, _tracking); + + log.Complete(); + return succ; + } + + private readonly Repository _repo; + private readonly Models.Branch _remoteBranch; + private string _worktreePath; + private string _branchName; + private bool _tracking; + } +} diff --git a/src/ViewModels/CherryPick.cs b/src/ViewModels/CherryPick.cs index fb8ae649fe..55ec83c99d 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 a215d5e28d..aed33c5090 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -2,10 +2,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace SourceGit.ViewModels { + public record TargetGroup(string Id, string Name, string Description); + public class Clone : Popup { [Required(ErrorMessage = "Remote URL is required")] @@ -46,20 +49,21 @@ public string Local set => SetProperty(ref _local, value); } - public List Groups + public List Groups { get; } - public RepositoryNode SelectedGroup + public int SelectedGroupIndex { - get => _selectedGroup; - set => SetProperty(ref _selectedGroup, value); + get => _selectedGroupIndex; + set => SetProperty(ref _selectedGroupIndex, value); } - public List Bookmarks + public bool CanAutoSelectGroup { - get; + get => _canAutoSelectGroup; + private set => SetProperty(ref _canAutoSelectGroup, value); } public int Bookmark @@ -83,20 +87,19 @@ public bool InitAndUpdateSubmodules public Clone(string pageId) { _pageId = pageId; + CanTerminate = true; - Groups = new List(); - Groups.Add(new RepositoryNode { Name = "No Group (Uncategorized)", Id = string.Empty }); - SelectedGroup = Groups[0]; + Groups = new List(); + Groups.Add(new TargetGroup(string.Empty, "Auto", "Based-on Default Clone Dir")); + Groups.Add(new TargetGroup(string.Empty, "No Group", "Uncategorized")); CollectGroups(Groups, Preferences.Instance.RepositoryNodes); - Bookmarks = new List(); - for (var i = 0; i < Models.Bookmarks.Brushes.Length; i++) - Bookmarks.Add(i); - var activeWorkspace = Preferences.Instance.GetActiveWorkspace(); - _parentFolder = activeWorkspace?.DefaultCloneDir; - if (string.IsNullOrEmpty(ParentFolder)) - _parentFolder = Preferences.Instance.GitDefaultCloneDir; + _defaultCloneDir = activeWorkspace?.DefaultCloneDir; + if (string.IsNullOrEmpty(_defaultCloneDir)) + _defaultCloneDir = Preferences.Instance.GitDefaultCloneDir; + + ParentFolder = _defaultCloneDir; } public static ValidationResult ValidateRemote(string remote, ValidationContext _) @@ -120,10 +123,14 @@ public override async Task Sure() var log = new CommandLog("Clone"); Use(log); + _cancellation = new CancellationTokenSource(); + var token = _cancellation.Token; + var succ = await new Commands.Clone(_pageId, _parentFolder, _remote, _local, _useSSH ? _sshKey : "", _extraArgs) + .WithCancellation(token) .Use(log) .ExecAsync(); - if (!succ) + if (!succ || token.IsCancellationRequested) return false; var path = _parentFolder; @@ -155,18 +162,38 @@ public override async Task Sure() .SetAsync("remote.origin.sshkey", _sshKey); } - if (InitAndUpdateSubmodules) + if (InitAndUpdateSubmodules && !token.IsCancellationRequested) { var submodules = await new Commands.QueryUpdatableSubmodules(path, true).GetResultAsync(); if (submodules.Count > 0) await new Commands.Submodule(path) + .WithCancellation(token) .Use(log) - .UpdateAsync(submodules, true); + .UpdateAsync(submodules, true, true, false); } log.Complete(); - var parent = _selectedGroup is { Id: not "" } ? _selectedGroup : null; + RepositoryNode parent = null; + if (_selectedGroupIndex == 0) // Auto (Based-on Default Clone Dir) + { + if (!string.IsNullOrEmpty(_defaultCloneDir)) + { + var normalizedDefaultCloneDir = _defaultCloneDir.Replace('\\', '/').TrimEnd('/') + "/"; + var normalizedParentFolder = _parentFolder.Replace('\\', '/').TrimEnd('/') + "/"; + if (normalizedParentFolder.Length > normalizedDefaultCloneDir.Length && + normalizedParentFolder.StartsWith(normalizedDefaultCloneDir, StringComparison.Ordinal)) + { + var relativePath = normalizedParentFolder.Substring(normalizedDefaultCloneDir.Length); + parent = Preferences.Instance.FindOrCreateGroupRecursive(relativePath.TrimEnd('/')); + } + } + } + else if (_selectedGroupIndex > 0 && _selectedGroupIndex < Groups.Count) // Existing group + { + parent = Preferences.Instance.FindNode(Groups[_selectedGroupIndex].Id); + } + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, parent, true); node.Bookmark = _bookmark; await node.UpdateStatusAsync(false, null); @@ -184,17 +211,26 @@ public override async Task Sure() Welcome.Instance.Refresh(); launcher.OpenRepositoryInTab(node, page); + + _cancellation = null; return true; } - private void CollectGroups(List outs, List collections) + public override void Terminate() + { + // Just fire cancel event and UI will auto wait the `Sure` complete + var _ = _cancellation?.CancelAsync(); + } + + private void CollectGroups(List outs, List collections, string prefix = null) { foreach (var node in collections) { if (!node.IsRepository) { - outs.Add(node); - CollectGroups(outs, node.SubNodes); + var displayName = prefix != null ? $"{prefix}/{node.Name}" : node.Name; + outs.Add(new(node.Id, displayName, string.Empty)); + CollectGroups(outs, node.SubNodes, displayName); } } } @@ -204,9 +240,12 @@ private void CollectGroups(List outs, List colle private bool _useSSH = false; private string _sshKey = string.Empty; private string _parentFolder = string.Empty; + private string _defaultCloneDir = string.Empty; private string _local = string.Empty; private string _extraArgs = string.Empty; - private RepositoryNode _selectedGroup = null; + private int _selectedGroupIndex = 0; + private bool _canAutoSelectGroup = false; private int _bookmark = 0; + private CancellationTokenSource _cancellation = null; } } diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 0f5931ff7d..636aad6654 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -40,9 +40,10 @@ 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])); + if (DiffContext == null) + UpdateDetails(); } } } @@ -78,12 +79,6 @@ public List WebLinks private set; } - public List Children - { - get => _children; - private set => SetProperty(ref _children, value); - } - public List Changes { get => _changes; @@ -96,18 +91,13 @@ public List VisibleChanges set => SetProperty(ref _visibleChanges, value); } - public List SelectedChanges + public ChangeSelection ChangeSelection { - get => _selectedChanges; + get => _changeSelection; set { - if (SetProperty(ref _selectedChanges, value)) - { - if (ActiveTabIndex != 1 || value is not { Count: 1 }) - DiffContext = null; - else - DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, value[0]), _diffContext); - } + if (SetProperty(ref _changeSelection, value)) + UpdateDetails(); } } @@ -174,6 +164,14 @@ public CommitDetail(Repository repo, CommitDetailSharedData sharedData) WebLinks = Models.CommitLink.Get(repo.Remotes); } + public CommitDetail Clone() + { + var cloned = new CommitDetail(_repo, null); + cloned.ActiveTabIndex = ActiveTabIndex; + cloned.Commit = _commit; + return cloned; + } + public void NavigateTo(string commitSHA) { _repo?.NavigateToCommit(commitSHA); @@ -462,7 +460,6 @@ private void Refresh() ViewRevisionFileContent = null; ViewRevisionFilePath = string.Empty; CanOpenRevisionFileWithDefaultEditor = false; - Children = null; RevisionFileSearchFilter = string.Empty; RevisionFileSearchSuggestion = null; ScrollOffset = Vector.Zero; @@ -471,7 +468,7 @@ private void Refresh() { Changes = []; VisibleChanges = []; - SelectedChanges = null; + ChangeSelection = new(null); return; } @@ -509,22 +506,12 @@ 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 cmd = new Commands.CompareRevisions(_repo.FullPath, _commit.FirstParentToCompare, _commit.SHA) { CancellationToken = token }; - var changes = await cmd.ReadAsync().ConfigureAwait(false); + var changes = await new Commands.CompareRevisions(_repo.FullPath, _commit.FirstParentToCompare, _commit.SHA) + .ReadAsync() + .ConfigureAwait(false); + var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) { @@ -544,14 +531,22 @@ private void Refresh() VisibleChanges = visible; if (visible.Count == 0) - SelectedChanges = null; + ChangeSelection = new(null); else - SelectedChanges = [VisibleChanges[0]]; + ChangeSelection = new(VisibleChanges.GetRange(0, 1)); }); } }, token); } + private void UpdateDetails() + { + if (ActiveTabIndex == 1 && _changeSelection is { Count: 1, HasFolder: false }) + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, _changeSelection.Changes[0]), _diffContext); + else + DiffContext = null; + } + private async Task ParseInlinesInMessageAsync(string message) { var inlines = new Models.InlineElementCollector(); @@ -695,13 +690,13 @@ private async Task SetViewingBlobAsync(Models.Object file) else { var size = await new Commands.QueryFileSize(_repo.FullPath, file.Path, _commit.SHA).GetResultAsync(); - ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size }; + ViewRevisionFileContent = new Models.RevisionBinaryFile(_repo.FullPath, file.Path, _commit.SHA, size); } return; } - var contentStream = await Commands.QueryFileContent.RunAsync(_repo.FullPath, _commit.SHA, file.Path); + await using var contentStream = await Commands.QueryFileContent.RunAsync(_repo.FullPath, _commit.SHA, file.Path); var content = await new StreamReader(contentStream).ReadToEndAsync(); var lfs = Models.LFSObject.Parse(content); if (lfs != null) @@ -720,25 +715,8 @@ 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) - { - 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 } - }; - } + var submoduleRoot = Path.Combine(_repo.FullPath, file.Path).Replace('\\', '/').TrimEnd('/'); + ViewRevisionFileContent = await new Commands.QuerySubmoduleRevision(submoduleRoot, file.SHA).GetResultAsync(); } [GeneratedRegex(@"\b(https?://|ftp://)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]")] @@ -755,10 +733,9 @@ private async Task SetViewingCommitAsync(Models.Object file) 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; + private ChangeSelection _changeSelection = new(null); private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; private string _viewRevisionFilePath = string.Empty; diff --git a/src/ViewModels/Compare.cs b/src/ViewModels/Compare.cs index f81d523d0c..e30a534458 100644 --- a/src/ViewModels/Compare.cs +++ b/src/ViewModels/Compare.cs @@ -9,10 +9,22 @@ namespace SourceGit.ViewModels { public class Compare : ObservableObject { - public bool IsLoading + public bool IsLoadingChanges { - get => _isLoading; - private set => SetProperty(ref _isLoading, value); + 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 @@ -56,15 +68,15 @@ public List VisibleChanges private set => SetProperty(ref _visibleChanges, value); } - public List SelectedChanges + public ChangeSelection ChangeSelection { - get => _selectedChanges; + get => _changeSelection; set { - if (SetProperty(ref _selectedChanges, value)) + if (SetProperty(ref _changeSelection, value)) { - if (value is { Count: 1 }) - DiffContext = new DiffContext(_repo, new Models.DiffOption(_based, _to, value[0]), _diffContext); + if (value is { Count: 1, HasFolder: false }) + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_based, _to, value.Changes[0]), _diffContext); else DiffContext = null; } @@ -77,7 +89,7 @@ public string SearchFilter set { if (SetProperty(ref _searchFilter, value)) - RefreshVisible(); + RefreshVisibleChanges(); } } @@ -87,43 +99,46 @@ public DiffContext 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.FullPath; + _repo = repo; _canResetFiles = !repo.IsBare; _based = GetSHA(based); _to = GetSHA(to); _baseName = GetName(based); _toName = GetName(to); - Refresh(); + _baseHead = new Commands.QuerySingleCommit(_repo.FullPath, _based).GetResult(); + _toHead = new Commands.QuerySingleCommit(_repo.FullPath, _to).GetResult(); + + UpdatePickableCommits(); + UpdateChanges(); } 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() { (_based, _to) = (_to, _based); (BaseName, ToName) = (_toName, _baseName); - - if (_baseHead != null) - (BaseHead, ToHead) = (_toHead, _baseHead); - - Refresh(); + (BaseHead, ToHead) = (_toHead, _baseHead); + (LeftOnlyCommits, RightOnlyCommits) = (_rightOnlyCommits, _leftOnlyCommits); + UpdateChanges(); } public void ClearSearchFilter() @@ -133,12 +148,12 @@ public void ClearSearchFilter() public string GetAbsPath(string path) { - return Native.OS.GetAbsPath(_repo, path); + return Native.OS.GetAbsPath(_repo.FullPath, path); } public void OpenInExternalDiffTool(Models.Change change) { - new Commands.DiffTool(_repo, new Models.DiffOption(_based, _to, change)).Open(); + new Commands.DiffTool(_repo.FullPath, new Models.DiffOption(_based, _to, change)).Open(); } public async Task ResetToLeftAsync(Models.Change change) @@ -148,21 +163,21 @@ public async Task ResetToLeftAsync(Models.Change change) if (change.Index == Models.ChangeState.Added) { - var fullpath = Native.OS.GetAbsPath(_repo, change.Path); + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); if (File.Exists(fullpath)) - await new Commands.Remove(_repo, [change.Path]).ExecAsync(); + await new Commands.Remove(_repo.FullPath, [change.Path]).ExecAsync(); } else if (change.Index == Models.ChangeState.Renamed) { - var renamed = Native.OS.GetAbsPath(_repo, change.Path); + var renamed = Native.OS.GetAbsPath(_repo.FullPath, change.Path); if (File.Exists(renamed)) - await new Commands.Remove(_repo, [change.Path]).ExecAsync(); + await new Commands.Remove(_repo.FullPath, [change.Path]).ExecAsync(); - await new Commands.Checkout(_repo).FileWithRevisionAsync(change.OriginalPath, _baseHead.SHA); + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.OriginalPath, _baseHead.SHA); } else { - await new Commands.Checkout(_repo).FileWithRevisionAsync(change.Path, _baseHead.SHA); + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.Path, _baseHead.SHA); } } @@ -170,21 +185,21 @@ public async Task ResetToRightAsync(Models.Change change) { if (change.Index == Models.ChangeState.Deleted) { - var fullpath = Native.OS.GetAbsPath(_repo, change.Path); + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); if (File.Exists(fullpath)) - await new Commands.Remove(_repo, [change.Path]).ExecAsync(); + await new Commands.Remove(_repo.FullPath, [change.Path]).ExecAsync(); } else if (change.Index == Models.ChangeState.Renamed) { - var old = Native.OS.GetAbsPath(_repo, change.OriginalPath); + var old = Native.OS.GetAbsPath(_repo.FullPath, change.OriginalPath); if (File.Exists(old)) - await new Commands.Remove(_repo, [change.OriginalPath]).ExecAsync(); + await new Commands.Remove(_repo.FullPath, [change.OriginalPath]).ExecAsync(); - await new Commands.Checkout(_repo).FileWithRevisionAsync(change.Path, ToHead.SHA); + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.Path, ToHead.SHA); } else { - await new Commands.Checkout(_repo).FileWithRevisionAsync(change.Path, ToHead.SHA); + await new Commands.Checkout(_repo.FullPath).FileWithRevisionAsync(change.Path, ToHead.SHA); } } @@ -197,13 +212,13 @@ public async Task ResetMultipleToLeftAsync(List changes) { if (c.Index == Models.ChangeState.Added) { - var fullpath = Native.OS.GetAbsPath(_repo, c.Path); + 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, c.Path); + var old = Native.OS.GetAbsPath(_repo.FullPath, c.Path); if (File.Exists(old)) removes.Add(c.Path); @@ -216,10 +231,10 @@ public async Task ResetMultipleToLeftAsync(List changes) } if (removes.Count > 0) - await new Commands.Remove(_repo, removes).ExecAsync(); + await new Commands.Remove(_repo.FullPath, removes).ExecAsync(); if (checkouts.Count > 0) - await new Commands.Checkout(_repo).MultipleFilesWithRevisionAsync(checkouts, _baseHead.SHA); + await new Commands.Checkout(_repo.FullPath).MultipleFilesWithRevisionAsync(checkouts, _baseHead.SHA); } public async Task ResetMultipleToRightAsync(List changes) @@ -231,13 +246,13 @@ public async Task ResetMultipleToRightAsync(List changes) { if (c.Index == Models.ChangeState.Deleted) { - var fullpath = Native.OS.GetAbsPath(_repo, c.Path); + 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, c.OriginalPath); + var renamed = Native.OS.GetAbsPath(_repo.FullPath, c.OriginalPath); if (File.Exists(renamed)) removes.Add(c.OriginalPath); @@ -250,43 +265,55 @@ public async Task ResetMultipleToRightAsync(List changes) } if (removes.Count > 0) - await new Commands.Remove(_repo, removes).ExecAsync(); + await new Commands.Remove(_repo.FullPath, removes).ExecAsync(); if (checkouts.Count > 0) - await new Commands.Checkout(_repo).MultipleFilesWithRevisionAsync(checkouts, _toHead.SHA); + await new Commands.Checkout(_repo.FullPath).MultipleFilesWithRevisionAsync(checkouts, _toHead.SHA); } public async Task SaveChangesAsPatchAsync(List changes, string saveTo) { - return await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _based, _to, saveTo); + return await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes, _based, _to, saveTo); } - private void Refresh() + public void CherryPick(List commits) { - IsLoading = true; - VisibleChanges = []; - SelectedChanges = []; + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new CherryPick(_repo, commits)); + } + + private void UpdatePickableCommits() + { + IsLoadingPickableCommits = true; Task.Run(async () => { - if (_baseHead == null) - { - var baseHead = await new Commands.QuerySingleCommit(_repo, _based) - .GetResultAsync() - .ConfigureAwait(false); + var rightOnly = await new Commands.QueryPickableCommits(_repo.FullPath, _baseHead.SHA, _toHead.SHA) + .GetResultAsync() + .ConfigureAwait(false); - var toHead = await new Commands.QuerySingleCommit(_repo, _to) - .GetResultAsync() - .ConfigureAwait(false); + var leftOnly = await new Commands.QueryPickableCommits(_repo.FullPath, _toHead.SHA, _baseHead.SHA) + .GetResultAsync() + .ConfigureAwait(false); - Dispatcher.UIThread.Post(() => - { - BaseHead = baseHead; - ToHead = toHead; - }); - } + Dispatcher.UIThread.Post(() => + { + LeftOnlyCommits = leftOnly; + RightOnlyCommits = rightOnly; + IsLoadingPickableCommits = false; + }); + }); + } + + private void UpdateChanges() + { + IsLoadingChanges = true; + VisibleChanges = []; + ChangeSelection = new(null); - _changes = await new Commands.CompareRevisions(_repo, _based, _to) + Task.Run(async () => + { + _changes = await new Commands.CompareRevisions(_repo.FullPath, _based, _to) .ReadAsync() .ConfigureAwait(false); @@ -305,17 +332,15 @@ private void Refresh() { TotalChanges = _changes.Count; VisibleChanges = visible; - IsLoading = false; + IsLoadingChanges = false; if (VisibleChanges.Count > 0) - SelectedChanges = [VisibleChanges[0]]; - else - SelectedChanges = []; + ChangeSelection = new(VisibleChanges.GetRange(0, 1)); }); }); } - private void RefreshVisible() + private void RefreshVisibleChanges() { if (_changes == null) return; @@ -359,8 +384,10 @@ private string GetSHA(object obj) }; } - private string _repo; - private bool _isLoading = true; + 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; @@ -371,7 +398,9 @@ private string GetSHA(object obj) private int _totalChanges = 0; private List _changes = null; private List _visibleChanges = null; - private List _selectedChanges = null; + private ChangeSelection _changeSelection = new(null); + private List _leftOnlyCommits = []; + private List _rightOnlyCommits = []; private string _searchFilter = string.Empty; private DiffContext _diffContext = null; } diff --git a/src/ViewModels/Conflict.cs b/src/ViewModels/Conflict.cs index 380ad68dcf..d98187d353 100644 --- a/src/ViewModels/Conflict.cs +++ b/src/ViewModels/Conflict.cs @@ -1,9 +1,11 @@ using System.IO; using System.Threading.Tasks; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class Conflict + public class Conflict : ObservableObject { public string Marker { @@ -15,29 +17,23 @@ 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 CanMerge + public object Mine { - get; - private set; - } = false; + get => _mine; + private set => SetProperty(ref _mine, value); + } public Conflict(Repository repo, WorkingCopy wc, Models.Change change) { @@ -45,22 +41,32 @@ public Conflict(Repository repo, WorkingCopy wc, Models.Change change) _wc = wc; _change = change; - CanMerge = _change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified; - if (CanMerge) - CanMerge = !Directory.Exists(Path.Combine(repo.FullPath, change.Path)); // Cannot merge directories (submodules) + Task.Run(async () => + { + _head = new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResult(); - if (CanMerge) - IsResolved = new Commands.IsConflictResolved(repo.FullPath, change).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"), + }; - _head = new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResult(); - (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() @@ -75,18 +81,21 @@ public async Task UseMineAsync() public MergeConflictEditor CreateOpenMergeEditorRequest() { - return CanMerge ? new MergeConflictEditor(_repo, _head, _change.Path) : null; + return _state == Models.ConflictFileState.UnmergedText ? new MergeConflictEditor(_repo, _head, _change.Path) : null; } public async Task MergeExternalAsync() { - if (CanMerge) + if (_state == Models.ConflictFileState.UnmergedText) await _wc.UseExternalMergeToolAsync(_change); } private Repository _repo = null; private WorkingCopy _wc = null; - private Models.Commit _head = 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/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index 8060f20f26..02dec49416 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -7,7 +7,6 @@ namespace SourceGit.ViewModels public class CreateBranch : Popup { [Required(ErrorMessage = "Branch name is required!")] - [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(CreateBranch), nameof(ValidateBranchName))] public string Name { @@ -77,7 +76,9 @@ public CreateBranch(Repository repo, Models.Branch branch) Name = branch.Name; BasedOn = branch; - DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; UpdateOverrideTip(); } @@ -89,7 +90,9 @@ public CreateBranch(Repository repo, Models.Commit commit) _head = commit.SHA; BasedOn = commit; - DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; UpdateOverrideTip(); } @@ -101,7 +104,9 @@ public CreateBranch(Repository repo, Models.Tag tag) _head = tag.SHA; BasedOn = tag; - DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; UpdateOverrideTip(); } @@ -109,6 +114,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is CreateBranch creator) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + if (!creator._allowOverwrite) { foreach (var b in creator._repo.Branches) @@ -221,6 +229,10 @@ public override async Task Sure() created.Upstream = basedOn.FullName; } + else + { + created.Upstream = string.Empty; + } _repo.RefreshAfterCreateBranch(created, CheckoutAfterCreated); } diff --git a/src/ViewModels/CreateTag.cs b/src/ViewModels/CreateTag.cs index ff729aab9c..0b8a3433af 100644 --- a/src/ViewModels/CreateTag.cs +++ b/src/ViewModels/CreateTag.cs @@ -13,7 +13,6 @@ public object BasedOn } [Required(ErrorMessage = "Tag name is required!")] - [RegularExpression(@"^(?!\.)(?!/)(?!.*\.$)(?!.*/$)(?!.*\.\.)[\w\-\+\./]+$", ErrorMessage = "Bad tag name format!")] [CustomValidation(typeof(CreateTag), nameof(ValidateTagName))] public string TagName { @@ -74,6 +73,9 @@ public static ValidationResult ValidateTagName(string name, ValidationContext ct { if (ctx.ObjectInstance is CreateTag creator) { + if (!Models.RefName.IsValidTagName(name)) + return new ValidationResult("Bad tag name format!"); + var found = creator._repo.Tags.Find(x => x.Name == name); if (found != null) return new ValidationResult("A tag with same name already exists!"); @@ -99,12 +101,13 @@ public override async Task Sure() else succ = await cmd.AddAsync(_basedOn); - if (succ && remotes != null) + if (succ && remotes is { Count: > 0 }) { + var fullname = $"refs/tags/{_tagName}"; foreach (var remote in remotes) - await new Commands.Push(_repo.FullPath, remote.Name, $"refs/tags/{_tagName}", false) + await new Commands.Push(_repo.FullPath, remote, fullname, false) .Use(log) - .RunAsync(); + .ExecAsync(); } log.Complete(); diff --git a/src/ViewModels/DeleteBranch.cs b/src/ViewModels/DeleteBranch.cs index 8d88210796..483f321dde 100644 --- a/src/ViewModels/DeleteBranch.cs +++ b/src/ViewModels/DeleteBranch.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -9,21 +10,26 @@ public Models.Branch Target get; } - public Models.Branch TrackingRemoteBranch + public Models.Branch Upstream { get; } - public string DeleteTrackingRemoteTip + public string DeleteUpstreamTip { get; - private set; } - public bool AlsoDeleteTrackingRemote + public bool DeleteUpstream { - get => _alsoDeleteTrackingRemote; - set => SetProperty(ref _alsoDeleteTrackingRemote, value); + get; + set; + } + + public bool Force + { + get; + set; } public DeleteBranch(Repository repo, Models.Branch branch) @@ -33,9 +39,12 @@ public DeleteBranch(Repository repo, Models.Branch 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); + var upstream = _repo.Branches.Find(x => x.FullName.Equals(branch.Upstream, StringComparison.Ordinal)); + if (upstream != null && upstream.Name.Equals(branch.Name, StringComparison.Ordinal)) + { + Upstream = upstream; + DeleteUpstreamTip = App.Text("DeleteBranch.WithTrackingRemote", upstream.FriendlyName); + } } } @@ -47,49 +56,66 @@ 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) - .Use(log) - .DeleteLocalAsync(); - _repo.UIStates.RemoveHistoryFilter(Target.FullName, Models.FilterType.LocalBranch); - - if (_alsoDeleteTrackingRemote && TrackingRemoteBranch != null) + do { - await DeleteRemoteBranchAsync(TrackingRemoteBranch, log); - _repo.UIStates.RemoveHistoryFilter(TrackingRemoteBranch.FullName, Models.FilterType.RemoteBranch); - } + succ = await new Commands.Branch(_repo.FullPath, Target.Name) + .Use(log) + .DeleteLocalAsync(Force); + + if (!succ) + break; + + _repo.UIStates.RemoveHistoryFilter(Target.FullName, Models.FilterType.LocalBranch); + + if (!DeleteUpstream || Upstream == null) + break; + + succ = await DeleteRemoteBranchAsync(Upstream, log); + if (!succ) + break; + + _repo.UIStates.RemoveHistoryFilter(Upstream.FullName, Models.FilterType.RemoteBranch); + } while (false); } else { - await DeleteRemoteBranchAsync(Target, log); - _repo.UIStates.RemoveHistoryFilter(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); + var exists = false; + var remote = _repo.Remotes.Find(x => x.Name.Equals(branch.Remote, StringComparison.Ordinal)); + if (remote != null) + { + exists = await new Commands.DoesBranchExistOnRemote(_repo.FullPath, remote, branch) + .Use(log) + .GetResultAsync() + .ConfigureAwait(false); + } if (exists) - await new Commands.Push(_repo.FullPath, branch.Remote, $"refs/heads/{branch.Name}", true) + return await new Commands.Push(_repo.FullPath, remote, $"refs/heads/{branch.Name}", true) .Use(log) - .RunAsync() + .ExecAsync() .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 b5d762a649..566dbf71e4 100644 --- a/src/ViewModels/DeleteMultipleBranches.cs +++ b/src/ViewModels/DeleteMultipleBranches.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -10,6 +11,12 @@ public List Targets get; } + public bool Force + { + get; + set; + } = false; + public DeleteMultipleBranches(Repository repo, List branches, bool isLocal) { _repo = repo; @@ -30,22 +37,12 @@ public override async Task Sure() foreach (var target in Targets) await new Commands.Branch(_repo.FullPath, target.Name) .Use(log) - .DeleteLocalAsync(); + .DeleteLocalAsync(Force); } else { foreach (var target in Targets) - { - var exists = await new Commands.Remote(_repo.FullPath).HasBranchAsync(target.Remote, target.Name); - if (exists) - await new Commands.Push(_repo.FullPath, target.Remote, $"refs/heads/{target.Name}", true) - .Use(log) - .RunAsync(); - else - await new Commands.Branch(_repo.FullPath, target.Name) - .Use(log) - .DeleteRemoteAsync(target.Remote); - } + await DeleteRemoteBranchAsync(target, log); } log.Complete(); @@ -53,6 +50,30 @@ public override async Task Sure() return true; } + private async Task DeleteRemoteBranchAsync(Models.Branch branch, CommandLog log) + { + var exists = false; + var remote = _repo.Remotes.Find(x => x.Name.Equals(branch.Remote, StringComparison.Ordinal)); + if (remote != null) + { + exists = await new Commands.DoesBranchExistOnRemote(_repo.FullPath, remote, branch) + .Use(log) + .GetResultAsync() + .ConfigureAwait(false); + } + + if (exists) + return await new Commands.Push(_repo.FullPath, remote, $"refs/heads/{branch.Name}", true) + .Use(log) + .ExecAsync() + .ConfigureAwait(false); + else + return await new Commands.Branch(_repo.FullPath, branch.Name) + .Use(log) + .DeleteRemoteAsync(branch.Remote, Force) + .ConfigureAwait(false); + } + private Repository _repo = null; private bool _isLocal = false; } diff --git a/src/ViewModels/DeleteMultipleTags.cs b/src/ViewModels/DeleteMultipleTags.cs index a11901c342..4e62285aeb 100644 --- a/src/ViewModels/DeleteMultipleTags.cs +++ b/src/ViewModels/DeleteMultipleTags.cs @@ -36,12 +36,13 @@ public override async Task Sure() .Use(log) .DeleteAsync(); - if (succ && DeleteFromRemote) + if (succ && DeleteFromRemote && _repo.Remotes is { Count: > 0 } remotes) { - foreach (var r in _repo.Remotes) - await new Commands.Push(_repo.FullPath, r.Name, $"refs/tags/{tag.Name}", true) + var fullname = $"refs/tags/{tag.Name}"; + foreach (var r in remotes) + await new Commands.Push(_repo.FullPath, r, fullname, true) .Use(log) - .RunAsync(); + .ExecAsync(); } } diff --git a/src/ViewModels/DeleteTag.cs b/src/ViewModels/DeleteTag.cs index 6dc6ebb9cd..26ad95e34b 100644 --- a/src/ViewModels/DeleteTag.cs +++ b/src/ViewModels/DeleteTag.cs @@ -27,7 +27,6 @@ public override async Task Sure() using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Deleting tag '{Target.Name}' ..."; - var remotes = PushToRemotes ? _repo.Remotes : []; var log = _repo.CreateLog("Delete Tag"); Use(log); @@ -35,12 +34,13 @@ public override async Task Sure() .Use(log) .DeleteAsync(); - if (succ) + if (succ && PushToRemotes && _repo.Remotes is { Count: > 0 } remotes) { + var fullname = $"refs/tags/{Target.Name}"; foreach (var r in remotes) - await new Commands.Push(_repo.FullPath, r.Name, $"refs/tags/{Target.Name}", true) + await new Commands.Push(_repo.FullPath, r, fullname, true) .Use(log) - .RunAsync(); + .ExecAsync(); } log.Complete(); diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 25155771c4..ddbdffbb60 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(); - } - } + get => _oldMode; + private set => SetProperty(ref _oldMode, value); } - public bool UseSideBySide + public int NewMode { - 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(); - } - } - } - - public string FileModeChange - { - 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,103 +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) - { - submoduleDiff.FullPath = submoduleRoot; - 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 { @@ -269,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); @@ -283,35 +196,161 @@ 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) { - if (!File.Exists(Path.Combine(repo, ".git"))) - return new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = sha } }; + var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath; + var imgDiff = new Models.ImageDiff(); + var fullPath = Path.Combine(_repo, _option.Path); - var uncommittedChangesCount = 0; - if (sha.EndsWith("-dirty", StringComparison.Ordinal)) + if (_option.Revisions.Count == 2) // Two revisions are specified, compare them { - sha = sha.Substring(0, sha.Length - 6); - uncommittedChangesCount = await new Commands.CountLocalChanges(repo, true).GetResultAsync().ConfigureAwait(false); + if (_option.Revisions[0].Equals("-R", StringComparison.Ordinal)) // `-R` means the old side is the working tree + { + 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; + } + + if (string.IsNullOrEmpty(_option.Revisions[1])) // Empty string in the second revision means the new side is the working tree + { + var newImage = await ImageSource.FromFileAsync(fullPath, imgDecoder).ConfigureAwait(false); + imgDiff.New = newImage.Bitmap; + imgDiff.NewFileSize = newImage.Size; + } + else + { + var newImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[1], _option.Path, imgDecoder).ConfigureAwait(false); + imgDiff.New = newImage.Bitmap; + imgDiff.NewFileSize = newImage.Size; + } + } + else if (_option.IsUnstaged) // Unstaged change compared to staged or HEAD + { + if (!oldPath.Equals("/dev/null", StringComparison.Ordinal)) + { + var oldImage = await ImageSource.FromRevisionAsync(_repo, string.Empty, 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; + } + else // Staged change compared to the last commit (HEAD) + { + var oldImage = await ImageSource.FromRevisionAsync(_repo, "HEAD", oldPath, imgDecoder).ConfigureAwait(false); + imgDiff.Old = oldImage.Bitmap; + imgDiff.OldFileSize = oldImage.Size; + + var newImage = await ImageSource.FromRevisionAsync(_repo, string.Empty, oldPath, imgDecoder).ConfigureAwait(false); + imgDiff.New = newImage.Bitmap; + imgDiff.NewFileSize = newImage.Size; } - var commit = await new Commands.QuerySingleCommit(repo, sha).GetResultAsync().ConfigureAwait(false); - if (commit == null) - return new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = sha } }; + 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); + + binaryDiff.Repository = _repo; + binaryDiff.FilePath = _option.Path; + + if (_option.Revisions.Count == 2) // Two revisions are specified, compare them + { + if (_option.Revisions[0].Equals("-R", StringComparison.Ordinal)) // `-R` means the old side is the working tree + binaryDiff.OldSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; + else + binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, _option.Revisions[0]).GetResultAsync().ConfigureAwait(false); - var body = await new Commands.QueryCommitFullMessage(repo, sha).GetResultAsync().ConfigureAwait(false); - return new Models.RevisionSubmodule() + if (string.IsNullOrEmpty(_option.Revisions[1])) // Empty string in the second revision means the new side is the working tree + { + binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; + binaryDiff.NewRevision = null; + } + else + { + binaryDiff.NewSize = await new Commands.QueryFileSize(_repo, _option.Path, _option.Revisions[1]).GetResultAsync().ConfigureAwait(false); + binaryDiff.NewRevision = _option.Revisions[1]; + } + } + else if (_option.IsUnstaged) // Unstaged change compared to staged or HEAD { - Commit = commit, - FullMessage = new Models.CommitFullMessage { Message = body }, - UncommittedChanges = uncommittedChangesCount - }; + if (!oldPath.Equals("/dev/null", StringComparison.Ordinal)) + binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, string.Empty).GetResultAsync().ConfigureAwait(false); + + binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0; + binaryDiff.NewRevision = null; + } + else // Staged change compared to the last commit (HEAD) + { + binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, "HEAD").GetResultAsync().ConfigureAwait(false); + binaryDiff.NewSize = await new Commands.QueryFileSize(_repo, _option.Path, string.Empty).GetResultAsync().ConfigureAwait(false); + binaryDiff.NewRevision = string.Empty; + } + + return binaryDiff; + } + + private async Task CreateSubmoduleDiffAsync(string oldRevision, string newRevision) + { + 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; + } + + private bool IsValidSubmoduleHash(string hash) + { + for (int i = 0; i < hash.Length; i++) + { + 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 @@ -341,12 +380,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 d4d37f25f5..a1448009c7 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 0a37b56bac..0b60f2f989 100644 --- a/src/ViewModels/Discard.cs +++ b/src/ViewModels/Discard.cs @@ -55,10 +55,11 @@ public Discard(Repository repo) Mode = new DiscardAllMode(); } - public Discard(Repository repo, List changes) + public Discard(Repository repo, List changes, Models.Change next) { _repo = repo; _changes = changes; + _next = next; if (_changes == null) Mode = new DiscardAllMode(); @@ -87,11 +88,17 @@ public override async Task Sure() } log.Complete(); + + var selection = new ChangeSelection(null); + if (_next != null) + selection.Changes.Add(_next); + _repo.WorkingCopy.SelectedUnstaged = selection; _repo.MarkWorkingCopyDirtyManually(); return true; } private readonly Repository _repo = null; private readonly List _changes = null; + private readonly Models.Change _next = null; } } diff --git a/src/ViewModels/DropHead.cs b/src/ViewModels/DropHead.cs deleted file mode 100644 index 37d253a0cb..0000000000 --- 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", false); - 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/EditRemote.cs b/src/ViewModels/EditRemote.cs index 84b950e2af..a8c37d781e 100644 --- a/src/ViewModels/EditRemote.cs +++ b/src/ViewModels/EditRemote.cs @@ -44,6 +44,17 @@ public string SSHKey set => SetProperty(ref _sshkey, value, true); } + public bool IsResetPushURLVisible + { + get => !string.IsNullOrEmpty(_remote.PushURL); + } + + public bool ResetPushURL + { + get; + set; + } + public EditRemote(Repository repo, Models.Remote remote) { _repo = repo; @@ -51,9 +62,8 @@ public EditRemote(Repository repo, Models.Remote remote) _name = remote.Name; _url = remote.URL; _useSSH = Models.Remote.IsSSH(remote.URL); - - if (_useSSH) - _sshkey = new Commands.Config(repo.FullPath).Get($"remote.{remote.Name}.sshkey"); + _sshkey = remote.PrivateSSHKey; + ResetPushURL = true; } public static ValidationResult ValidateRemoteName(string name, ValidationContext ctx) @@ -112,16 +122,26 @@ public override async Task Sure() if (_remote.URL != _url) { - var succ = await new Commands.Remote(_repo.FullPath).SetURLAsync(_name, _url, false); + var succ = await new Commands.Remote(_repo.FullPath).SetURLAsync(_name, _url, false, false); if (succ) _remote.URL = _url; } - var pushURL = await new Commands.Remote(_repo.FullPath).GetURLAsync(_name, true); - if (pushURL != _url) - await new Commands.Remote(_repo.FullPath).SetURLAsync(_name, _url, true); + if (IsResetPushURLVisible && ResetPushURL) + { + var succ = await new Commands.Remote(_repo.FullPath).SetURLAsync(_name, _remote.PushURL, true, true); + if (succ) + _remote.PushURL = null; + } + + // Update SSH key + { + var key = _useSSH ? SSHKey : null; + var succ = await new Commands.Config(_repo.FullPath).SetAsync($"remote.{_name}.sshkey", key); + if (succ) + _remote.PrivateSSHKey = key; + } - await new Commands.Config(_repo.FullPath).SetAsync($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); return true; } diff --git a/src/ViewModels/EditRepositoryNode.cs b/src/ViewModels/EditRepositoryNode.cs index cb83668e7e..40402ef67d 100644 --- a/src/ViewModels/EditRepositoryNode.cs +++ b/src/ViewModels/EditRepositoryNode.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -16,11 +15,6 @@ public bool IsRepository get; } - public List Bookmarks - { - get; - } - [Required(ErrorMessage = "Name is required!")] public string Name { @@ -42,9 +36,6 @@ public EditRepositoryNode(RepositoryNode node) Target = node.IsRepository ? node.Id : node.Name; IsRepository = node.IsRepository; - Bookmarks = new List(); - for (var i = 0; i < Models.Bookmarks.Brushes.Length; i++) - Bookmarks.Add(i); } public override Task Sure() diff --git a/src/ViewModels/ExecuteCustomAction.cs b/src/ViewModels/ExecuteCustomAction.cs index b59b8f67ed..5ff87b3bc5 100644 --- a/src/ViewModels/ExecuteCustomAction.cs +++ b/src/ViewModels/ExecuteCustomAction.cs @@ -181,7 +181,7 @@ public ExecuteCustomAction(Repository repo, Models.CustomAction action, object s { _repo = repo; CustomAction = action; - Target = scopeTarget ?? new Models.Null(); + Target = scopeTarget ?? Models.Null.Instance; foreach (var ctl in CustomAction.Controls) { @@ -240,14 +240,21 @@ private string PrepareStringByTarget(string org) 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), - _ => org.Replace("${BRANCH}", _repo.CurrentBranch?.Name ?? "HEAD") + Models.CustomActionTargetFile f => org.Replace("${SHA}", f.Revision?.SHA ?? "HEAD").Replace("${FILE}", f.File), + _ => org }; } diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 3236a64217..330dd5d763 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -48,6 +49,7 @@ public Fetch(Repository repo, Models.Remote preferredRemote = null) _repo = repo; IsFetchAllRemoteVisible = repo.Remotes.Count > 1 && preferredRemote == null; _fetchAllRemotes = IsFetchAllRemoteVisible && _repo.UIStates.FetchAllRemotes; + CanTerminate = true; if (preferredRemote != null) { @@ -68,7 +70,7 @@ public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); - var navigateToUpstreamHEAD = _repo.IsHistoriesVisible && + var navigateToUpstreamHEAD = _repo.IsDashboardVisible && _repo.Histories.SelectedCommits.Count == 1 && _repo.Histories.SelectedCommits[0].IsCurrentHead; @@ -77,23 +79,33 @@ public override async Task Sure() var log = _repo.CreateLog("Fetch"); Use(log); + _cancellation = new CancellationTokenSource(); + var token = _cancellation.Token; + if (FetchAllRemotes) { foreach (var remote in _repo.Remotes) - await new Commands.Fetch(_repo.FullPath, remote.Name, notags, force) + { + await new Commands.Fetch(_repo.FullPath, remote, notags, force) + .WithCancellation(token) .Use(log) - .RunAsync(); + .ExecAsync(); + + if (token.IsCancellationRequested) + break; + } } else { - await new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, notags, force) + await new Commands.Fetch(_repo.FullPath, SelectedRemote, notags, force) + .WithCancellation(token) .Use(log) - .RunAsync(); + .ExecAsync(); } log.Complete(); - if (navigateToUpstreamHEAD) + if (navigateToUpstreamHEAD && !token.IsCancellationRequested) { var upstream = _repo.CurrentBranch?.Upstream; if (!string.IsNullOrEmpty(upstream)) @@ -103,11 +115,21 @@ public override async Task Sure() } } - _repo.MarkFetched(); + if (!token.IsCancellationRequested) + _repo.MarkFetched(); + + _cancellation = null; return true; } + public override void Terminate() + { + // Just fire cancel event and UI will auto wait the `Sure` complete + var _ = _cancellation?.CancelAsync(); + } + private readonly Repository _repo = null; private bool _fetchAllRemotes = false; + private CancellationTokenSource _cancellation = null; } } diff --git a/src/ViewModels/FetchInto.cs b/src/ViewModels/FetchInto.cs index bcf4612035..816687ec7a 100644 --- a/src/ViewModels/FetchInto.cs +++ b/src/ViewModels/FetchInto.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Threading; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -17,8 +19,11 @@ public Models.Branch Upstream public FetchInto(Repository repo, Models.Branch local, Models.Branch upstream) { _repo = repo; + _remote = repo.Remotes.Find(x => x.Name.Equals(upstream.Remote, StringComparison.Ordinal)); + Local = local; Upstream = upstream; + CanTerminate = true; } public override async Task Sure() @@ -29,21 +34,34 @@ public override async Task Sure() var log = _repo.CreateLog($"Fetch Into '{Local.FriendlyName}'"); Use(log); - await new Commands.Fetch(_repo.FullPath, Local, Upstream) + _cancellation = new CancellationTokenSource(); + var token = _cancellation.Token; + + await new Commands.Fetch(_repo.FullPath, _remote, Upstream, Local) + .WithCancellation(token) .Use(log) - .RunAsync(); + .ExecAsync(); log.Complete(); - if (_repo.SelectedViewIndex == 0) + if (_repo.SelectedViewIndex == 0 && !token.IsCancellationRequested) { var newHead = await new Commands.QueryRevisionByRefName(_repo.FullPath, Local.Name).GetResultAsync(); _repo.NavigateToCommit(newHead, true); } + _cancellation = null; return true; } + public override void Terminate() + { + // Just fire cancel event and UI will auto wait the `Sure` complete + var _ = _cancellation?.CancelAsync(); + } + private readonly Repository _repo = null; + private readonly Models.Remote _remote = null; + private CancellationTokenSource _cancellation = null; } } diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 17d1fc05d4..5cd54bef0f 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -127,11 +127,11 @@ private async Task GetRevisionFileContentAsync(Models.Object obj) } var size = await new Commands.QueryFileSize(_repo, _file, _revision.SHA).GetResultAsync().ConfigureAwait(false); - var binaryFile = new Models.RevisionBinaryFile() { Size = size }; + var binaryFile = new Models.RevisionBinaryFile(_repo, _file, _revision.SHA, size); return new FileHistoriesRevisionFile(_file, binaryFile, true); } - var contentStream = await Commands.QueryFileContent.RunAsync(_repo, _revision.SHA, _file).ConfigureAwait(false); + await using var contentStream = await Commands.QueryFileContent.RunAsync(_repo, _revision.SHA, _file).ConfigureAwait(false); var content = await new StreamReader(contentStream).ReadToEndAsync(); var lfs = Models.LFSObject.Parse(content); if (lfs != null) @@ -153,15 +153,8 @@ 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() - { - Commit = commit ?? new Models.Commit() { SHA = obj.SHA }, - FullMessage = new Models.CommitFullMessage { Message = message } - }; - + var submoduleRoot = Path.Combine(_repo, _file).Replace('\\', '/').TrimEnd('/'); + var module = await new Commands.QuerySubmoduleRevision(submoduleRoot, obj.SHA).GetResultAsync().ConfigureAwait(false); return new FileHistoriesRevisionFile(_file, module); } diff --git a/src/ViewModels/GitFlowFinish.cs b/src/ViewModels/GitFlowFinish.cs index 42cf054407..373e5398b3 100644 --- a/src/ViewModels/GitFlowFinish.cs +++ b/src/ViewModels/GitFlowFinish.cs @@ -15,6 +15,12 @@ public Models.GitFlowBranchType Type private set; } + public bool RebaseBeforeMerging + { + get; + set; + } = false; + public bool Squash { get; @@ -44,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, 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 ee8673aa3a..68f9f88e4c 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; @@ -18,7 +32,6 @@ public string Prefix } [Required(ErrorMessage = "Name is required!!!")] - [RegularExpression(@"^[\w\-/\.#]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(GitFlowStart), nameof(ValidateBranchName))] public string Name { @@ -32,12 +45,30 @@ 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) { if (ctx.ObjectInstance is GitFlowStart starter) { + if (!Models.RefName.IsValidBranchName($"{starter.Prefix}{name}")) + return new ValidationResult("Bad branch name format!"); + var check = $"{starter.Prefix}{name}"; foreach (var b in starter._repo.Branches) { @@ -57,12 +88,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 196ca6f1d5..128b53ff9d 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -18,6 +18,38 @@ public bool IsLoading set => SetProperty(ref _isLoading, value); } + public bool IsSearchingCommits + { + get => _isSearchingCommits; + set + { + if (value) + _repo.SelectedViewIndex = 0; + else + _searchCommitContext.EndSearch(); + + SetProperty(ref _isSearchingCommits, value); + } + } + + public SearchCommitContext SearchCommitContext + { + get => _searchCommitContext; + } + + public bool EnableTopoOrder + { + get => _repo.UIStates.EnableTopoOrderInHistory; + set + { + if (value != _repo.UIStates.EnableTopoOrderInHistory) + { + _repo.UIStates.EnableTopoOrderInHistory = value; + _repo.RefreshCommits(); + } + } + } + public bool IsAuthorColumnVisible { get => _repo.UIStates.IsAuthorColumnVisibleInHistory; @@ -44,14 +76,27 @@ public bool IsSHAColumnVisible } } - public bool IsDateTimeColumnVisible + public bool IsAuthorTimeColumnVisible { - get => _repo.UIStates.IsDateTimeColumnVisibleInHistory; + get => _repo.UIStates.IsAuthorTimeColumnVisibleInHistory; set { - if (_repo.UIStates.IsDateTimeColumnVisibleInHistory != value) + if (_repo.UIStates.IsAuthorTimeColumnVisibleInHistory != value) { - _repo.UIStates.IsDateTimeColumnVisibleInHistory = value; + _repo.UIStates.IsAuthorTimeColumnVisibleInHistory = value; + OnPropertyChanged(); + } + } + } + + public bool IsCommitTimeColumnVisible + { + get => _repo.UIStates.IsCommitTimeColumnVisibleInHistory; + set + { + if (_repo.UIStates.IsCommitTimeColumnVisibleInHistory != value) + { + _repo.UIStates.IsCommitTimeColumnVisibleInHistory = value; OnPropertyChanged(); } } @@ -62,6 +107,7 @@ public List Commits get => _commits; set { + GenerateGraph(value); if (SetProperty(ref _commits, value)) PostCommitsChanged(); } @@ -73,12 +119,26 @@ public Models.CommitGraph Graph set => SetProperty(ref _graph, value); } + public Models.CommitGraphHighlighting GraphHighlighting + { + get => _repo.UIStates.GraphHighlighting; + set + { + if (_repo.UIStates.GraphHighlighting != value) + { + _repo.UIStates.GraphHighlighting = value; + GenerateGraph(_commits); + } + } + } + public List SelectedCommits { get => _selectedCommits; set { - if (SetProperty(ref _selectedCommits, value)) + var oldCount = _selectedCommits.Count; + if (SetProperty(ref _selectedCommits, value) && oldCount + value.Count > 0) PostSelectedCommitsChanged(); } } @@ -86,7 +146,11 @@ public List SelectedCommits public object DetailContext { get => _detailContext; - set => SetProperty(ref _detailContext, value); + set + { + if (SetProperty(ref _detailContext, value)) + OnPropertyChanged(nameof(IsOpenAsStandaloneVisible)); + } } public Models.Bisect Bisect @@ -97,20 +161,14 @@ public Models.Bisect Bisect public Models.Branch CurrentBranch { - get => _repo.CurrentBranch; + get => _currentBranch; + set => SetProperty(ref _currentBranch, value); } - public bool HighlightCurrentBranchOnly + public bool HasSingleRemote { - get => _repo.UIStates.OnlyHighlightCurrentBranchInHistory; - set - { - if (_repo.UIStates.OnlyHighlightCurrentBranchInHistory != value) - { - _repo.UIStates.OnlyHighlightCurrentBranchInHistory = value; - OnPropertyChanged(); - } - } + get => _hasSingleRemote; + set => SetProperty(ref _hasSingleRemote, value); } public AvaloniaList IssueTrackers @@ -138,19 +196,57 @@ 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) { _repo = repo; _commitDetailSharedData = new CommitDetailSharedData(); + _searchCommitContext = new SearchCommitContext(repo.FullPath, this); + } + + public bool HasShowFlag(Models.HistoryShowFlags flag) + { + return _repo.UIStates.HistoryShowFlags.HasFlag(flag); } - public void NotifyCurrentBranchChanged() + public void ToggleShowFlag(Models.HistoryShowFlags flag) { - OnPropertyChanged(nameof(CurrentBranch)); + if (HasShowFlag(flag)) + _repo.UIStates.HistoryShowFlags -= flag; + else + _repo.UIStates.HistoryShowFlags |= flag; + _repo.RefreshCommits(); } public Models.BisectState UpdateBisectInfo() @@ -162,26 +258,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) @@ -227,6 +337,12 @@ public void NavigateTo(string commitSHA) .ConfigureAwait(false); } + public void CheckoutCommitDetached(Models.Commit c) + { + if (!c.IsCurrentHead && _repo.CanCreatePopup()) + _repo.ShowPopup(new CheckoutDetached(_repo, c)); + } + public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) { if (decorator == null) @@ -315,7 +431,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)); } } @@ -345,45 +461,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 GetCommitFullMessageAsync(Models.Commit commit) { return await new Commands.QueryCommitFullMessage(_repo.FullPath, commit.SHA) @@ -396,7 +473,7 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) var head = _commits.Find(x => x.IsCurrentHead); if (head == null) { - _repo.SearchCommitContext.Selected = null; + _searchCommitContext.Selected = null; head = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync(); if (head != null) DetailContext = new RevisionCompare(_repo, commit, head); @@ -417,7 +494,7 @@ private void PostCommitsChanged() if (_selectedCommits.Count == 0) return; - if (_commits.Count == 0 || _selectedCommits.Count > 2) + if (_commits.Count == 0 || _selectedCommits.Count > 20) { SelectedCommits = []; return; @@ -449,14 +526,17 @@ private void PostSelectedCommitsChanged() if (_selectedCommits.Count == 0) { - _repo.SearchCommitContext.Selected = null; - DetailContext = null; + _searchCommitContext.Selected = null; + DetailContext = Models.Null.Instance; } 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 (_isSearchingCommits) + { + if (_searchCommitContext.Selected == null || !_searchCommitContext.Selected.SHA.Equals(c.SHA, StringComparison.Ordinal)) + _searchCommitContext.Selected = _searchCommitContext.Results?.Find(x => x.SHA.Equals(c.SHA, StringComparison.Ordinal)); + } if (_detailContext is CommitDetail detail) detail.Commit = c; @@ -465,7 +545,7 @@ private void PostSelectedCommitsChanged() } else if (_selectedCommits.Count == 2) { - _repo.SearchCommitContext.Selected = null; + _searchCommitContext.Selected = null; if (_detailContext is RevisionCompare compare) compare.SetTargets(_selectedCommits[1], _selectedCommits[0]); @@ -474,24 +554,48 @@ private void PostSelectedCommitsChanged() } else { - _repo.SearchCommitContext.Selected = null; + _searchCommitContext.Selected = null; DetailContext = new Models.Count(_selectedCommits.Count); } + + if (_repo.UIStates.GraphHighlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + GenerateGraph(_commits); + } + + private void GenerateGraph(List commits) + { + 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, firstParentOnly, highlighting, extraHeads); } private Repository _repo = null; + private Models.Branch _currentBranch = null; + private bool _hasSingleRemote = false; private CommitDetailSharedData _commitDetailSharedData = null; private bool _isLoading = true; - private List _commits = new List(); + private List _commits = []; private Models.CommitGraph _graph = null; private List _selectedCommits = []; private Models.Bisect _bisect = null; - private object _detailContext = null; + private object _detailContext = Models.Null.Instance; private bool _ignoreSelectionChange = false; - 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 bool _isSearchingCommits = false; + private SearchCommitContext _searchCommitContext = null; + + 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/ImageSource.cs b/src/ViewModels/ImageSource.cs index 6adafd5f3a..561c8fdeb2 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -40,12 +40,19 @@ public static Models.ImageDecoder GetDecoder(string file) 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) { + 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); } @@ -164,11 +171,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(); + } } } @@ -198,14 +217,14 @@ private static ImageSource DecodeWithTiff(Stream stream, long 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 ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0); var pixelSize = new PixelSize(image.Width, image.Height); var dpi = new Vector(96, 96); - var bitmap = new Bitmap(PixelFormat.Rgba8888, AlphaFormat.Unpremul, ptr, pixelSize, dpi, stride); + 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 5b61035f36..97699eeab9 100644 --- a/src/ViewModels/InProgressContexts.cs +++ b/src/ViewModels/InProgressContexts.cs @@ -27,6 +27,12 @@ public async Task AbortAsync(CommandLog log) { if (_abortCmd != null) await _abortCmd.Use(log).ExecAsync(); + + OnAborted(); + } + + protected virtual void OnAborted() + { } protected Commands.Command _continueCmd = null; @@ -54,7 +60,8 @@ public CherryPickInProgress(Repository repo) { 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 @@ -101,6 +108,7 @@ public Models.Commit Onto public RebaseInProgress(Repository repo) { + _gitDir = repo.GitDir; Name = "Rebase"; _continueCmd = new Commands.Command @@ -108,7 +116,7 @@ public RebaseInProgress(Repository repo) 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 @@ -123,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(); @@ -143,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 @@ -160,7 +186,8 @@ public RevertInProgress(Repository repo) { 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 @@ -207,7 +234,8 @@ public MergeInProgress(Repository repo) { 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/InitGitFlow.cs b/src/ViewModels/InitGitFlow.cs index 6e425879a6..798120b55a 100644 --- a/src/ViewModels/InitGitFlow.cs +++ b/src/ViewModels/InitGitFlow.cs @@ -11,17 +11,15 @@ public partial class InitGitFlow : Popup [GeneratedRegex(@"^[\w\-/\.]+$")] private static partial Regex REG_TAG_PREFIX(); - [Required(ErrorMessage = "Master branch name is required!!!")] - [RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")] + [Required(ErrorMessage = "Production branch name is required!!!")] [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!!!")] - [RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")] + [Required(ErrorMessage = "Development branch name is required!!!")] [CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))] public string Develop { @@ -72,21 +70,24 @@ 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) + public static ValidationResult ValidateBaseBranch(string name, 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 (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + + if (initializer._production == initializer._develop) + return new ValidationResult("Develop branch has the same name with production branch!"); } return ValidationResult.Success; @@ -111,10 +112,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 +138,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 +159,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 a5572356de..a8f013fe4c 100644 --- a/src/ViewModels/InteractiveRebase.cs +++ b/src/ViewModels/InteractiveRebase.cs @@ -125,6 +125,12 @@ public bool AutoStash set; } = true; + public bool NoVerify + { + get; + set; + } + public AvaloniaList IssueTrackers { get => _repo.IssueTrackers; @@ -384,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(); diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 5beb3f1363..774fbe0e41 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -45,58 +45,36 @@ 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) - { - var node = pref.FindNode(repo) ?? - new RepositoryNode - { - Id = repo, - Name = Path.GetFileName(repo), - Bookmark = 0, - IsRepository = true, - }; - - OpenRepositoryInTab(node, null); - } + OpenRepositoryInTab(repo, null); _ignoreIndexChange = false; - if (TryOpenRepositoryFromPath(startupRepo)) - return; - - if (!string.IsNullOrEmpty(startupRepo)) - { - var test = new Commands.QueryRepositoryRootPath(startupRepo).GetResult(); - if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) - { - var node = pref.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); - Welcome.Instance.Refresh(); - - OpenRepositoryInTab(node, null); - return; - } - } - - var activeIdx = ActiveWorkspace.ActiveIdx; - if (activeIdx > 0 && activeIdx < Pages.Count) + if (!TryOpenRepositoryFromPath(startupRepo)) { - ActivePage = Pages[activeIdx]; - return; + var activeIdx = ActiveWorkspace.ActiveIdx; + if (activeIdx > 0 && activeIdx < Pages.Count) + ActivePage = Pages[activeIdx]; + else + ActivePage = Pages[0]; } - ActivePage = Pages[0]; PostActivePageChanged(); } @@ -104,6 +82,15 @@ public bool TryOpenRepositoryFromPath(string repo) { if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) { + var isBare = new Commands.IsBareRepository(repo).GetResult(); + if (isBare) + { + 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)) { @@ -112,6 +99,19 @@ public bool TryOpenRepositoryFromPath(string repo) OpenRepositoryInTab(node, null); return true; } + else + { + if (ActivePage is not { Data: Welcome { }, Popup: null }) + AddNewTab(); + + if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(repo, test.StdErr, out var safeDirectory)) + ActivePage.Popup = new TrustRepository(ActivePage.Node.Id, repo, test.StdErr, safeDirectory, null, false, true, 0); + else + ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository."); + + return true; + } } return false; @@ -149,18 +149,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); - } + OpenRepositoryInTab(repo, null); var activeIdx = to.ActiveIdx; if (activeIdx >= 0 && activeIdx < Pages.Count) @@ -230,6 +219,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(); @@ -298,6 +289,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) @@ -333,6 +339,9 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) return; } + if (node.IsUnmanaged) + node.LoadMinimalInfo(gitDir); + var repo = new Repository(isBare, node.Id, gitDir); repo.Open(); @@ -369,6 +378,72 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) ActivePage = page; } + public void OpenSubRepository(LauncherPage ownerPage, string fullpath) + { + var normalizedPath = fullpath.Replace('\\', '/').TrimEnd('/'); + + // Check if the sub-repository is already open in any of the tabs + foreach (var one in Pages) + { + if (one.Node.Id.Equals(normalizedPath, StringComparison.Ordinal)) + { + ActivePage = one; + return; + } + } + + // Make sure the target directory exists + if (!Directory.Exists(normalizedPath)) + { + ownerPage.Notifications.Add(new Models.Notification + { + Group = ownerPage.Node.Id, + Message = "Repository path does NOT exist. Please check the path.", + IsError = true, + }); + + return; + } + + // Check if the target directory is a valid git repository + var gitDir = GetRepositoryGitDir(normalizedPath); + if (string.IsNullOrEmpty(gitDir)) + { + ownerPage.Notifications.Add(new Models.Notification + { + Group = ownerPage.Node.Id, + Message = "Given path is not a valid git repository!", + IsError = true, + }); + return; + } + + // Get the owner repository's name and extract the pure owner name (without any prefix) + var ownerName = ownerPage.Node.Name; + var colonIdx = ownerName.LastIndexOf(':'); + var pureOwnerName = (colonIdx >= 0 && colonIdx < ownerName.Length - 1) ? ownerName.Substring(colonIdx + 1).Trim() : ownerName; + + // Find the sub-repository node in the preferences or create a new one if it doesn't exist + var node = Preferences.Instance.FindNode(normalizedPath) ?? new RepositoryNode + { + Id = normalizedPath, + Name = $"{pureOwnerName}: {Path.GetFileName(normalizedPath)}", + Bookmark = 0, + IsRepository = true, + IsUnmanaged = true + }; + node.LoadMinimalInfo(gitDir); + + var repo = new Repository(false, node.Id, gitDir); + repo.Open(); + + var page = new LauncherPage(node, repo); + var idxOfOwner = Pages.IndexOf(ownerPage); + Pages.Insert(idxOfOwner + 1, page); + _activeWorkspace.Repositories.Insert(idxOfOwner + 1, normalizedPath); + ActivePage = page; + } + private void DispatchNotification(Models.Notification notification) { if (!Dispatcher.UIThread.CheckAccess()) @@ -434,6 +509,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(); } @@ -466,5 +544,6 @@ private void PostActivePageChanged() private bool _ignoreIndexChange; private string _title = string.Empty; private ICommandPalette _commandPalette; + private Models.Version _newVersion = null; } } diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index db5e0241b4..370e5a25c7 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -114,6 +114,14 @@ public void CancelPopup() Popup = null; } + public void TerminatePopup() + { + if (_popup is not { CanTerminate: true, InProgress: true }) + return; + + _popup.Terminate(); + } + private RepositoryNode _node = null; private object _data = null; private Models.DirtyState _dirtyState = Models.DirtyState.None; diff --git a/src/ViewModels/LayoutInfo.cs b/src/ViewModels/LayoutInfo.cs index 1b78153aa5..43b58f5013 100644 --- a/src/ViewModels/LayoutInfo.cs +++ b/src/ViewModels/LayoutInfo.cs @@ -35,10 +35,22 @@ public WindowState LauncherWindowState set; } = WindowState.Normal; - public GridLength RepositorySidebarWidth + public double RepositorySidebarWidth { - get => _repositorySidebarWidth; - set => SetProperty(ref _repositorySidebarWidth, value); + get; + set; + } = 250; + + public double RepositorySearchCommitWidth + { + get; + set; + } = 250; + + public bool IsSidebarCollapsedInLocalChanges + { + get => _isSidebarCollapsedInLocalChanges; + set => SetProperty(ref _isSidebarCollapsedInLocalChanges, value); } public GridLength WorkingCopyLeftWidth @@ -65,17 +77,10 @@ public GridLength CommitDetailFilesLeftWidth set => SetProperty(ref _commitDetailFilesLeftWidth, value); } - public DataGridLength AuthorColumnWidth - { - get => _authorColumnWidth; - set => SetProperty(ref _authorColumnWidth, new DataGridLength(value.Value, DataGridLengthUnitType.Pixel, 0, value.DisplayValue)); - } - - 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, 0, 120); + private bool _isSidebarCollapsedInLocalChanges = false; } } diff --git a/src/ViewModels/Merge.cs b/src/ViewModels/Merge.cs index ed7936bfc4..35f9567f68 100644 --- a/src/ViewModels/Merge.cs +++ b/src/ViewModels/Merge.cs @@ -1,8 +1,18 @@ 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 @@ -21,9 +31,7 @@ public Models.MergeMode Mode set { if (SetProperty(ref _mode, value)) - CanEditMessage = _mode == Models.MergeMode.Default || - _mode == Models.MergeMode.FastForward || - _mode == Models.MergeMode.NoFastForward; + CanEditMessage = _mode == Models.MergeMode.Default || _mode == Models.MergeMode.NoFastForward; } } @@ -39,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; @@ -47,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) @@ -57,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) @@ -67,6 +86,8 @@ public Merge(Repository repo, Models.Tag source, string into) Source = source; Into = into; Mode = AutoSelectMergeMode(); + + Test(); } public override async Task Sure() @@ -127,9 +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/MergeConflictEditor.cs b/src/ViewModels/MergeConflictEditor.cs index 5c739ae23d..fc9d0f632d 100644 --- a/src/ViewModels/MergeConflictEditor.cs +++ b/src/ViewModels/MergeConflictEditor.cs @@ -190,6 +190,9 @@ public async Task SaveAndStageAsync() 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 diff --git a/src/ViewModels/MoveRepositoryNode.cs b/src/ViewModels/MoveRepositoryNode.cs index 0c213058bd..716274295f 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/OpenLocalRepository.cs b/src/ViewModels/OpenLocalRepository.cs index d83ecc07cc..8636980b49 100644 --- a/src/ViewModels/OpenLocalRepository.cs +++ b/src/ViewModels/OpenLocalRepository.cs @@ -16,20 +16,15 @@ public string RepoPath set => SetProperty(ref _repoPath, value, true); } - public List Groups + public List Groups { get; } - public RepositoryNode Group + public int SelectedGroupIndex { - get => _group; - set => SetProperty(ref _group, value); - } - - public List Bookmarks - { - get; + get => _selectedGroupIndex; + set => SetProperty(ref _selectedGroupIndex, value); } public int Bookmark @@ -42,14 +37,10 @@ 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]; + Groups = new List(); + Groups.Add(new TargetGroup(string.Empty, "Auto", "Based-on Default Clone Dir")); + Groups.Add(new TargetGroup(string.Empty, "No Group", "Uncategorized")); CollectGroups(Groups, Preferences.Instance.RepositoryNodes); - - Bookmarks = new List(); - for (var i = 0; i < Models.Bookmarks.Brushes.Length; i++) - Bookmarks.Add(i); } public static ValidationResult ValidateRepoPath(string folder, ValidationContext _) @@ -61,8 +52,32 @@ public static ValidationResult ValidateRepoPath(string folder, ValidationContext public override async Task Sure() { + RepositoryNode parent = null; + if (_selectedGroupIndex == 0) // Auto (Based-on Default Clone Dir) + { + var activeWorkspace = Preferences.Instance.GetActiveWorkspace(); + var defaultCloneDir = activeWorkspace?.DefaultCloneDir; + if (string.IsNullOrEmpty(defaultCloneDir)) + defaultCloneDir = Preferences.Instance.GitDefaultCloneDir; + + if (!string.IsNullOrEmpty(defaultCloneDir)) + { + var normalizedParentFolder = new DirectoryInfo(RepoPath).Parent!.FullName.Replace('\\', '/').TrimEnd('/') + "/"; + var normalizedDefaultCloneDir = defaultCloneDir.Replace('\\', '/').TrimEnd('/') + "/"; + if (normalizedParentFolder.Length > normalizedDefaultCloneDir.Length && + normalizedParentFolder.StartsWith(normalizedDefaultCloneDir, StringComparison.Ordinal)) + { + var relativePath = normalizedParentFolder.Substring(normalizedDefaultCloneDir.Length); + parent = Preferences.Instance.FindOrCreateGroupRecursive(relativePath.TrimEnd('/')); + } + } + } + else if (_selectedGroupIndex > 1 && _selectedGroupIndex < Groups.Count) // Existing group + { + parent = Preferences.Instance.FindNode(Groups[_selectedGroupIndex].Id); + } + var isBare = await new Commands.IsBareRepository(_repoPath).GetResultAsync(); - var parent = _group is { Id: not "" } ? _group : null; var repoRoot = _repoPath; if (!isBare) { @@ -74,12 +89,27 @@ public override async Task Sure() else { var launcher = App.GetLauncher(); - foreach (var page in launcher.Pages) + if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(_repoPath, test.StdErr, out var safeDirectory)) + { + foreach (var page in launcher.Pages) + { + if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + { + page.Popup = new TrustRepository(page.Node.Id, _repoPath, test.StdErr, safeDirectory, parent, true, true, _bookmark); + break; + } + } + } + else { - if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + foreach (var page in launcher.Pages) { - page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr); - break; + if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + { + page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr); + break; + } } } @@ -95,21 +125,22 @@ public override async Task Sure() return true; } - private void CollectGroups(List outs, List collections) + private void CollectGroups(List outs, List collections, string prefix = null) { foreach (var node in collections) { if (!node.IsRepository) { - outs.Add(node); - CollectGroups(outs, node.SubNodes); + var displayName = prefix != null ? $"{prefix}/{node.Name}" : node.Name; + outs.Add(new(node.Id, displayName, string.Empty)); + CollectGroups(outs, node.SubNodes, displayName); } } } private string _pageId = string.Empty; private string _repoPath = string.Empty; - private RepositoryNode _group = null; + private int _selectedGroupIndex = 0; private int _bookmark = 0; } } diff --git a/src/ViewModels/Popup.cs b/src/ViewModels/Popup.cs index 9d800c50c2..10bc44fe1b 100644 --- a/src/ViewModels/Popup.cs +++ b/src/ViewModels/Popup.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -6,6 +7,16 @@ namespace SourceGit.ViewModels { public class Popup : ObservableValidator, Models.ICommandLogReceiver { + public bool CanTerminate + { + get => _canTerminate; + protected set + { + if (OperatingSystem.IsWindows() || Native.OS.SupportSetSid()) + SetProperty(ref _canTerminate, value); + } + } + public bool InProgress { get => _inProgress; @@ -49,12 +60,17 @@ public virtual Task Sure() return null; } + public virtual void Terminate() + { + } + protected void Use(CommandLog log) { _log = log; _log.Subscribe(this); } + private bool _canTerminate = false; private bool _inProgress = false; private string _progressDescription = string.Empty; private CommandLog _log = null; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 36c9864e89..4803ec4296 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -193,18 +193,6 @@ 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; @@ -217,6 +205,12 @@ public bool ShowTagsInGraph set => SetProperty(ref _showTagsInGraph, value); } + public bool UseCompactBranchNamesInGraph + { + get => _useCompactBranchNamesInGraph; + set => SetProperty(ref _useCompactBranchNamesInGraph, value); + } + public bool UseTwoColumnsLayoutInHistories { get => _useTwoColumnsLayoutInHistories; @@ -243,17 +237,16 @@ 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; @@ -308,28 +301,10 @@ public bool EnableCompactFoldersInChangesTree set => SetProperty(ref _enableCompactFoldersInChangesTree, value); } - public Models.ChangeViewMode UnstagedChangeViewMode - { - get => _unstagedChangeViewMode; - set => SetProperty(ref _unstagedChangeViewMode, value); - } - - public Models.ChangeViewMode StagedChangeViewMode - { - get => _stagedChangeViewMode; - set => SetProperty(ref _stagedChangeViewMode, value); - } - - public Models.ChangeViewMode CommitChangeViewMode + public Models.ChangeViewMode ChangeViewMode { - get => _commitChangeViewMode; - set => SetProperty(ref _commitChangeViewMode, value); - } - - public Models.ChangeViewMode StashChangeViewMode - { - get => _stashChangeViewMode; - set => SetProperty(ref _stashChangeViewMode, value); + get => _changeViewMode; + set => SetProperty(ref _changeViewMode, value); } public string GitInstallPath @@ -469,12 +444,6 @@ public string ExternalMergeToolMergeArgs } } - public uint StatisticsSampleColor - { - get => _statisticsSampleColor; - set => SetProperty(ref _statisticsSampleColor, value); - } - public List RepositoryNodes { get; @@ -570,6 +539,20 @@ public RepositoryNode FindNode(string id) return FindNodeRecursive(id, RepositoryNodes); } + public RepositoryNode FindOrCreateGroupRecursive(string path) + { + List collection = RepositoryNodes; + RepositoryNode node = null; + + foreach (var name in path.Split('/')) + { + node = FindOrCreateGroupInCollection(collection, name); + collection = node.SubNodes; + } + + return node; + } + public RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent, bool shouldMoveNode, bool save = true) { var normalized = repo.Replace('\\', '/').TrimEnd('/'); @@ -652,17 +635,17 @@ public void Save() if (_isLoading || _isReadonly) return; - var tmpfile = Path.Combine(Native.OS.DataDir, "preference_tmp.json"); + var tmpfile = Path.Combine(Native.OS.BasicDirectories.ConfigDir, "preference_tmp.json"); var content = JsonSerializer.Serialize(this, JsonCodeGen.Default.Preferences); File.WriteAllText(tmpfile, content); - var finalFile = Path.Combine(Native.OS.DataDir, "preference.json"); + var finalFile = Path.Combine(Native.OS.BasicDirectories.ConfigDir, "preference.json"); File.Move(tmpfile, finalFile, true); } private static Preferences Load() { - var path = Path.Combine(Native.OS.DataDir, "preference.json"); + var path = Path.Combine(Native.OS.BasicDirectories.ConfigDir, "preference.json"); if (!File.Exists(path)) return new Preferences(); @@ -746,6 +729,27 @@ private RepositoryNode FindNodeRecursive(string id, List collect return null; } + private RepositoryNode FindOrCreateGroupInCollection(List collection, string name) + { + foreach (var node in collection) + { + if (!node.IsRepository && node.Name.Equals(name, StringComparison.Ordinal)) + return node; + } + + var added = new RepositoryNode() + { + Id = Guid.NewGuid().ToString(), + Name = name, + IsRepository = false, + IsExpanded = true, + }; + collection.Add(added); + + SortNodes(collection); + return added; + } + private List FindNodeContainer(RepositoryNode node, List collection) { foreach (var sub in collection) @@ -819,8 +823,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; @@ -831,6 +834,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; @@ -838,14 +842,9 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private int _lfsImageActiveIdx = 0; private int _imageDiffActiveIdx = 0; private bool _enableCompactFoldersInChangesTree = false; - - private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List; - private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List; - private Models.ChangeViewMode _commitChangeViewMode = Models.ChangeViewMode.List; - private Models.ChangeViewMode _stashChangeViewMode = Models.ChangeViewMode.List; + private Models.ChangeViewMode _changeViewMode = Models.ChangeViewMode.List; private string _gitDefaultCloneDir = string.Empty; private int _shellOrTerminalType = -1; - private uint _statisticsSampleColor = 0xFF00FF00; } } diff --git a/src/ViewModels/PruneRemote.cs b/src/ViewModels/PruneRemote.cs index cba2213cc3..507c49494d 100644 --- a/src/ViewModels/PruneRemote.cs +++ b/src/ViewModels/PruneRemote.cs @@ -25,7 +25,7 @@ public override async Task Sure() var succ = await new Commands.Remote(_repo.FullPath) .Use(log) - .PruneAsync(Remote.Name); + .PruneAsync(Remote.Name, Remote.PrivateSSHKey); log.Complete(); return succ; diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index a136f321e3..7aaf1be4f4 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Threading; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -47,7 +49,7 @@ public Models.DealWithLocalChanges DealWithLocalChanges { get; set; - } = Models.DealWithLocalChanges.DoNothing; + } public bool UseRebase { @@ -59,6 +61,11 @@ public Pull(Repository repo, Models.Branch specifiedRemoteBranch) { _repo = repo; Current = repo.CurrentBranch; + CanTerminate = true; + + DealWithLocalChanges = Preferences.Instance.UseStashAndReapplyByDefault ? + Models.DealWithLocalChanges.StashAndReapply : + Models.DealWithLocalChanges.DoNothing; if (specifiedRemoteBranch != null) { @@ -112,6 +119,9 @@ public override async Task Sure() var log = _repo.CreateLog("Pull"); Use(log); + _cancellation = new CancellationTokenSource(); + var token = _cancellation.Token; + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); var needPopStash = false; if (changes > 0) @@ -127,6 +137,7 @@ public override async Task Sure() { log.Complete(); _repo.MarkWorkingCopyDirtyManually(); + _cancellation = null; return false; } @@ -138,17 +149,26 @@ public override async Task Sure() } } - bool rs = await new Commands.Pull( - _repo.FullPath, - _selectedRemote.Name, - !string.IsNullOrEmpty(Current.Upstream) && Current.Upstream.Equals(_selectedBranch.FullName) ? string.Empty : _selectedBranch.Name, - UseRebase).Use(log).RunAsync(); - if (rs) + bool rs = false; + if (!token.IsCancellationRequested) { - await _repo.AutoUpdateSubmodulesAsync(log); + var target = _selectedBranch; + if (!string.IsNullOrEmpty(Current.Upstream) && target.FullName.Equals(Current.Upstream, StringComparison.Ordinal)) + target = null; + + rs = await new Commands.Pull( + _repo.FullPath, + _selectedRemote, + target, + UseRebase).WithCancellation(token).Use(log).ExecAsync(); - if (needPopStash) - await new Commands.Stash(_repo.FullPath).Use(log).PopAsync("stash@{0}"); + if (rs) + { + await _repo.AutoUpdateSubmodulesAsync(log); + + if (needPopStash) + await new Commands.Stash(_repo.FullPath).Use(log).PopAsync("stash@{0}"); + } } log.Complete(); @@ -159,56 +179,41 @@ public override async Task Sure() _repo.NavigateToCommit(head, true); } + _cancellation = null; return rs; } + public override void Terminate() + { + // Just fire cancel event and UI will auto wait the `Sure` complete + var _ = _cancellation?.CancelAsync(); + } + private void PostRemoteSelected() { var remoteName = _selectedRemote.Name; var branches = new List(); foreach (var branch in _repo.Branches) { - if (branch.Remote == remoteName) + if (!branch.IsLocal && branch.Remote.Equals(remoteName, StringComparison.Ordinal)) branches.Add(branch); } - RemoteBranches = branches; + Models.Branch selected = null; + if (!string.IsNullOrEmpty(Current.Upstream)) + selected = branches.Find(x => x.FullName.Equals(Current.Upstream, StringComparison.Ordinal)); - var autoSelectedBranch = false; - if (!string.IsNullOrEmpty(Current.Upstream) && - Current.Upstream.StartsWith($"refs/remotes/{remoteName}/", System.StringComparison.Ordinal)) - { - foreach (var branch in branches) - { - if (Current.Upstream == branch.FullName) - { - SelectedBranch = branch; - autoSelectedBranch = true; - break; - } - } - } - - if (!autoSelectedBranch) - { - foreach (var branch in branches) - { - if (Current.Name == branch.Name) - { - SelectedBranch = branch; - autoSelectedBranch = true; - break; - } - } - } + if (selected == null) + selected = branches.Find(x => x.Name.Equals(Current.Name, StringComparison.Ordinal)); - if (!autoSelectedBranch) - SelectedBranch = null; + RemoteBranches = branches; + SelectedBranch = selected; } private readonly Repository _repo = null; private Models.Remote _selectedRemote = null; private List _remoteBranches = null; private Models.Branch _selectedBranch = null; + private CancellationTokenSource _cancellation = null; } } diff --git a/src/ViewModels/Push.cs b/src/ViewModels/Push.cs index 2da3895b77..ee7b660ac4 100644 --- a/src/ViewModels/Push.cs +++ b/src/ViewModels/Push.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Threading; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -13,6 +14,11 @@ public bool HasSpecifiedLocalBranch private set; } + public List LocalBranches + { + get; + } + [Required(ErrorMessage = "Local branch is required!!!")] public Models.Branch SelectedLocalBranch { @@ -20,18 +26,14 @@ public Models.Branch SelectedLocalBranch set { if (SetProperty(ref _selectedLocalBranch, value, true)) - AutoSelectBranchByRemote(); + PostLocalBranchChanged(); } } - public List LocalBranches - { - get; - } - public List Remotes { - get => _repo.Remotes; + get => _remotes; + private set => SetProperty(ref _remotes, value); } [Required(ErrorMessage = "Remote is required!!!")] @@ -41,7 +43,7 @@ public Models.Remote SelectedRemote set { if (SetProperty(ref _selectedRemote, value, true)) - AutoSelectBranchByRemote(); + PostSelectedRemoteChanged(); } } @@ -58,7 +60,11 @@ public Models.Branch SelectedRemoteBranch set { if (SetProperty(ref _selectedRemoteBranch, value, true)) - IsSetTrackOptionVisible = value != null && (value.Head == null || _selectedLocalBranch.Upstream != value.FullName); + { + IsSetTrackOptionVisible = !string.IsNullOrEmpty(_selectedRemote.URL) + && value != null + && (value.Head == null || _selectedLocalBranch.Upstream != value.FullName); + } } } @@ -97,9 +103,16 @@ public bool ForcePush set; } + public bool NoVerify + { + get; + set; + } + public Push(Repository repo, Models.Branch localBranch) { _repo = repo; + CanTerminate = true; // Gather all local branches and find current branch. LocalBranches = new List(); @@ -118,46 +131,14 @@ public Push(Repository repo, Models.Branch localBranch) if (LocalBranches.Count == 0) LocalBranches.Add(localBranch); - _selectedLocalBranch = localBranch; HasSpecifiedLocalBranch = true; + SelectedLocalBranch = localBranch; } else { - _selectedLocalBranch = current; HasSpecifiedLocalBranch = false; + SelectedLocalBranch = current; } - - // Find preferred remote if selected local branch has upstream. - if (!string.IsNullOrEmpty(_selectedLocalBranch?.Upstream) && !_selectedLocalBranch.IsUpstreamGone) - { - _tracking = false; - - foreach (var branch in repo.Branches) - { - if (!branch.IsLocal && _selectedLocalBranch.Upstream == branch.FullName) - { - _selectedRemote = repo.Remotes.Find(x => x.Name == branch.Remote); - break; - } - } - } - else - { - _tracking = true; - } - - // Set default remote to the first if it has not been set. - if (_selectedRemote == null) - { - Models.Remote remote = null; - if (!string.IsNullOrEmpty(_repo.Settings.DefaultRemote)) - remote = repo.Remotes.Find(x => x.Name == _repo.Settings.DefaultRemote); - - _selectedRemote = remote ?? repo.Remotes[0]; - } - - // Auto select preferred remote branch. - AutoSelectBranchByRemote(); } public void PushToNewBranch(string name) @@ -189,43 +170,148 @@ public override bool CanStartDirectly() public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); - - var remoteBranchName = _selectedRemoteBranch.Name; - ProgressDescription = $"Push {_selectedLocalBranch.Name} -> {_selectedRemote.Name}/{remoteBranchName} ..."; + ProgressDescription = $"Push {_selectedLocalBranch.Name} -> {_selectedRemoteBranch.FriendlyName} ..."; var log = _repo.CreateLog("Push"); Use(log); + _cancellation = new CancellationTokenSource(); + var token = _cancellation.Token; + var succ = await new Commands.Push( _repo.FullPath, - _selectedLocalBranch.Name, - _selectedRemote.Name, - remoteBranchName, + _selectedLocalBranch, + _selectedRemote, + _selectedRemoteBranch, PushAllTags, _repo.Submodules.Count > 0 && CheckSubmodules, _isSetTrackOptionVisible && _tracking, - ForcePush).Use(log).RunAsync(); + ForcePush, + NoVerify).WithCancellation(token).Use(log).ExecAsync(); log.Complete(); + + _cancellation = null; return succ; } - private void AutoSelectBranchByRemote() + public override void Terminate() { + // Just fire cancel event and UI will auto wait the `Sure` complete + var _ = _cancellation?.CancelAsync(); + } + + private void PostLocalBranchChanged() + { + if (_selectedLocalBranch == null) + return; + + var remotes = new List(); + remotes.AddRange(_repo.Remotes); + + // Respect the `branch..pushRemote` settings. + _preferredPushRemote = new Commands.Config(_repo.FullPath).Get($"branch.\"{_selectedLocalBranch.Name}\".pushRemote"); + if (!string.IsNullOrEmpty(_preferredPushRemote)) + { + var remote = remotes.Find(IsPreferredPushRemote); + if (remote == null) + { + var extra = new Models.Remote() { Name = _preferredPushRemote }; + remotes.Add(extra); + + Remotes = remotes; + ForceUpdateSelectedRemote(extra); + } + else + { + // Force to trigger `PostRemoteChanged` even if the remote is the same as before. + Remotes = remotes; + ForceUpdateSelectedRemote(remote); + } + + return; + } + + // Update remotes list. + Remotes = remotes; + + // Try to select remote by upstream branch. + var upstream = _selectedLocalBranch.Upstream; + if (!string.IsNullOrEmpty(upstream) && !_selectedLocalBranch.IsUpstreamGone) + { + foreach (var branch in _repo.Branches) + { + if (!branch.IsLocal && upstream.Equals(branch.FullName, StringComparison.Ordinal)) + { + ForceUpdateSelectedRemote(Remotes.Find(x => x.Name == branch.Remote)); + return; + } + } + } + + // Fallback to select the first remote. + if (Remotes.Count > 0) + { + Models.Remote fallback = null; + if (!string.IsNullOrEmpty(_repo.Settings.DefaultRemote)) + fallback = Remotes.Find(x => x.Name.Equals(_repo.Settings.DefaultRemote, StringComparison.Ordinal)); + + ForceUpdateSelectedRemote(fallback ?? Remotes[0]); + } + } + + private void ForceUpdateSelectedRemote(Models.Remote remote) + { + var old = _selectedRemote; + SelectedRemote = remote; + if (remote == old) + PostSelectedRemoteChanged(); + } + + private void PostSelectedRemoteChanged() + { + 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); } + // Check `branch..merge` configuration if selected remote comes from an extra `branch..pushRemote` + if (!string.IsNullOrEmpty(_preferredPushRemote) && IsPreferredPushRemote(_selectedRemote)) + { + var mergeTarget = new Commands.Config(_repo.FullPath).Get($"branch.\"{_selectedLocalBranch.Name}\".merge"); + if (!string.IsNullOrEmpty(mergeTarget)) + { + var target = branches.Find(x => x.FullName.Equals(mergeTarget, StringComparison.Ordinal)); + if (target == null) + { + target = new Models.Branch() + { + Name = mergeTarget, + Remote = _selectedRemote.Name, + Head = "---", // Not used by `git push` command but used by Views + }; + + branches.Add(target); + } + + RemoteBranches = branches; + SelectedRemoteBranch = target; + return; + } + } + // If selected local branch has upstream. Try to find it in current remote branches. if (!string.IsNullOrEmpty(_selectedLocalBranch.Upstream)) { foreach (var branch in branches) { - if (_selectedLocalBranch.Upstream == branch.FullName) + if (_selectedLocalBranch.Upstream.Equals(branch.FullName, StringComparison.Ordinal)) { RemoteBranches = branches; SelectedRemoteBranch = branch; @@ -237,7 +323,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; @@ -256,12 +342,21 @@ private void AutoSelectBranchByRemote() SelectedRemoteBranch = fake; } + private bool IsPreferredPushRemote(Models.Remote remote) + { + return remote.Name.Equals(_preferredPushRemote, StringComparison.Ordinal) || + remote.URL.Equals(_preferredPushRemote, StringComparison.Ordinal); + } + private readonly Repository _repo = null; + private List _remotes = null; private Models.Branch _selectedLocalBranch = null; private Models.Remote _selectedRemote = null; + private string _preferredPushRemote = null; private List _remoteBranches = []; private Models.Branch _selectedRemoteBranch = null; private bool _isSetTrackOptionVisible = false; - private bool _tracking = true; + private bool _tracking = false; + private CancellationTokenSource _cancellation = null; } } diff --git a/src/ViewModels/PushRevision.cs b/src/ViewModels/PushRevision.cs index 4d49b52724..6db65ae763 100644 --- a/src/ViewModels/PushRevision.cs +++ b/src/ViewModels/PushRevision.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -27,6 +28,7 @@ public PushRevision(Repository repo, Models.Commit revision, Models.Branch remot Revision = revision; RemoteBranch = remoteBranch; Force = false; + CanTerminate = true; } public override async Task Sure() @@ -37,20 +39,30 @@ public override async Task Sure() var log = _repo.CreateLog("Push Revision"); Use(log); + _cancellation = new CancellationTokenSource(); + var token = _cancellation.Token; + + var remote = _repo.Remotes.Find(x => x.Name.Equals(RemoteBranch.Name, StringComparison.Ordinal)); var succ = await new Commands.Push( _repo.FullPath, - Revision.SHA, - RemoteBranch.Remote, - RemoteBranch.Name, - false, - false, - false, - Force).Use(log).RunAsync(); + Revision, + remote, + RemoteBranch, + Force).WithCancellation(token).Use(log).ExecAsync(); log.Complete(); + + _cancellation = null; return succ; } + public override void Terminate() + { + // Just fire cancel event and UI will auto wait the `Sure` complete + var _ = _cancellation?.CancelAsync(); + } + private readonly Repository _repo; + private CancellationTokenSource _cancellation = null; } } diff --git a/src/ViewModels/PushTag.cs b/src/ViewModels/PushTag.cs index d595482547..b9f31c7979 100644 --- a/src/ViewModels/PushTag.cs +++ b/src/ViewModels/PushTag.cs @@ -48,18 +48,18 @@ public override async Task Sure() { foreach (var remote in _repo.Remotes) { - succ = await new Commands.Push(_repo.FullPath, remote.Name, tag, false) + succ = await new Commands.Push(_repo.FullPath, remote, tag, false) .Use(log) - .RunAsync(); + .ExecAsync(); if (!succ) break; } } else { - succ = await new Commands.Push(_repo.FullPath, SelectedRemote.Name, tag, false) + succ = await new Commands.Push(_repo.FullPath, SelectedRemote, tag, false) .Use(log) - .RunAsync(); + .ExecAsync(); } log.Complete(); diff --git a/src/ViewModels/PushToNewBranch.cs b/src/ViewModels/PushToNewBranch.cs new file mode 100644 index 0000000000..95f55f30c7 --- /dev/null +++ b/src/ViewModels/PushToNewBranch.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class PushToNewBranch : ObservableValidator + { + public string Remote + { + get; + } + + [Required(ErrorMessage = "Branch name is required!")] + [CustomValidation(typeof(PushToNewBranch), nameof(ValidateBranchName))] + public string BranchName + { + get => _branchName; + set => SetProperty(ref _branchName, value, true); + } + + public PushToNewBranch(string remote) + { + Remote = remote; + } + + public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) + { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + + return ValidationResult.Success; + } + + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode")] + public bool Check() + { + ValidateAllProperties(); + return !HasErrors; + } + + private string _branchName = string.Empty; + } +} diff --git a/src/ViewModels/Rebase.cs b/src/ViewModels/Rebase.cs index 315e3c6347..2213a55af2 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/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index dbca651e72..72d1a4115a 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -12,7 +12,6 @@ public Models.Branch Target } [Required(ErrorMessage = "Branch name is required!!!")] - [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))] public string Name { @@ -31,6 +30,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is RenameBranch rename) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + foreach (var b in rename._repo.Branches) { if (b.IsLocal && b != rename.Target && b.Name.Equals(name, StringComparison.Ordinal)) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index e9c573bf71..aa58226d97 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -51,6 +51,19 @@ public Models.FilterMode 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; @@ -63,7 +76,7 @@ public int SelectedViewIndex { if (SetProperty(ref _selectedViewIndex, value)) { - OnPropertyChanged(nameof(IsHistoriesVisible)); + OnPropertyChanged(nameof(IsDashboardVisible)); OnPropertyChanged(nameof(IsWorkingCopyVisible)); OnPropertyChanged(nameof(IsStashesVisible)); } @@ -85,7 +98,7 @@ public StashesPage StashesPage get => _stashesPage; } - public bool IsHistoriesVisible + public bool IsDashboardVisible { get => SelectedViewIndex == 0; } @@ -100,38 +113,6 @@ public bool IsStashesVisible get => SelectedViewIndex == 2; } - public bool EnableTopoOrderInHistory - { - get => _uiStates.EnableTopoOrderInHistory; - set - { - if (value != _uiStates.EnableTopoOrderInHistory) - { - _uiStates.EnableTopoOrderInHistory = value; - RefreshCommits(); - } - } - } - - public Models.HistoryShowFlags HistoryShowFlags - { - get => _uiStates.HistoryShowFlags; - private set - { - if (value != _uiStates.HistoryShowFlags) - { - _uiStates.HistoryShowFlags = value; - RefreshCommits(); - } - } - } - - public bool HighlightCurrentBranchOnlyInHistory - { - get => _histories.HighlightCurrentBranchOnly; - set => _histories.HighlightCurrentBranchOnly = value; - } - public string Filter { get => _filter; @@ -151,7 +132,14 @@ public string Filter public List Remotes { get => _remotes; - private set => SetProperty(ref _remotes, value); + private set + { + if (SetProperty(ref _remotes, value)) + { + if (_histories != null) + _histories.HasSingleRemote = value != null && value.Count == 1; + } + } } public List Branches @@ -168,7 +156,9 @@ private set var oldHead = _currentBranch?.Head; if (SetProperty(ref _currentBranch, value)) { - _histories.NotifyCurrentBranchChanged(); + if (_histories != null) + _histories.CurrentBranch = value; + if (value != null && !value.Head.Equals(oldHead, StringComparison.Ordinal) && _workingCopy is { UseAmend: true }) _workingCopy.UseAmend = false; } @@ -277,26 +267,6 @@ public bool IncludeUntracked } } - public bool IsSearchingCommits - { - get => _isSearchingCommits; - set - { - if (SetProperty(ref _isSearchingCommits, value)) - { - if (value) - SelectedViewIndex = 0; - else - _searchCommitContext.EndSearch(); - } - } - } - - public SearchCommitContext SearchCommitContext - { - get => _searchCommitContext; - } - public bool IsLocalBranchGroupExpanded { get => _uiStates.IsLocalBranchesExpandedInSideBar; @@ -406,6 +376,11 @@ public InProgressContext InProgressContext get => _workingCopy?.InProgressContext; } + public bool IsSkippingOrAbortingMerge + { + get => _workingCopy is { InProgressContext: { }, IsCommitting: true }; + } + public Models.BisectState BisectState { get => _bisectState; @@ -458,13 +433,13 @@ public Repository(bool isBare, string path, string gitDir) { _gitCommonDir = GitDir; } - } - public void Open() - { _settings = Models.RepositorySettings.Get(_gitCommonDir); _uiStates = Models.RepositoryUIStates.Load(GitDir); + } + public void Open() + { try { _watcher = new Models.Watcher(this, FullPath, _gitCommonDir); @@ -478,7 +453,6 @@ public void Open() _histories = new Histories(this); _workingCopy = new WorkingCopy(this) { CommitMessage = _uiStates.LastCommitMessage }; _stashesPage = new StashesPage(this); - _searchCommitContext = new SearchCommitContext(this); _selectedViewIndex = Preferences.Instance.ShowLocalChangesByDefault ? 1 : 0; _lastFetchTime = DateTime.Now; _autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000); @@ -542,8 +516,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) @@ -563,7 +537,7 @@ public Models.GitFlowBranchType GetGitFlowType(Models.Branch b) public bool IsLFSEnabled() { - var path = Path.Combine(FullPath, ".git", "hooks", "pre-push"); + var path = Path.Combine(GitDir, "hooks", "pre-push"); if (!File.Exists(path)) return false; @@ -637,6 +611,18 @@ public CommandLog CreateLog(string name) return log; } + public string GetRecommandedWorktreeDir() + { + var commonDirFile = Path.Combine(GitDir, "commondir"); + var isWorktree = GitDir.IndexOf("/worktrees/", StringComparison.Ordinal) > 0 && File.Exists(commonDirFile); + var parentFolder = Path.GetFullPath(Path.Combine(FullPath, "..")); + if (isWorktree) + return parentFolder; + + var dirName = $"{Path.GetFileName(FullPath)}-worktrees"; + return Path.Combine(parentFolder, dirName); + } + public void RefreshAll() { RefreshCommits(); @@ -660,17 +646,7 @@ 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; + GitFlow.Parse(config); }); } @@ -775,8 +751,8 @@ public IDisposable LockWatcher() 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) @@ -804,15 +780,21 @@ public void RefreshAfterCreateBranch(Models.Branch created, bool checkout) CurrentBranch = created; } - List locals = []; + 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, []); + var builder = BuildBranchTree(locals, [], false); LocalBranchTrees = builder.Locals; + LocalBranchesCount = count; RefreshCommits(); RefreshWorkingCopyChanges(); @@ -822,7 +804,6 @@ public void RefreshAfterCreateBranch(Models.Branch created, bool checkout) public void RefreshAfterCheckoutBranch(Models.Branch checkouted) { _watcher?.MarkBranchUpdated(); - _watcher?.MarkWorkingCopyUpdated(); if (_currentBranch.IsDetachedHead) { @@ -846,9 +827,10 @@ public void RefreshAfterCheckoutBranch(Models.Branch checkouted) locals.Add(b); } - var builder = BuildBranchTree(locals, []); + var builder = BuildBranchTree(locals, [], false); LocalBranchTrees = builder.Locals; CurrentBranch = checkouted; + GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !checkouted.IsTrackStatusVisible); RefreshCommits(); RefreshWorkingCopyChanges(); @@ -862,8 +844,28 @@ public void RefreshAfterRenameBranch(Models.Branch b, string newName) var newFullName = $"refs/heads/{newName}"; _uiStates.RenameBranchFilter(b.FullName, newFullName); - b.Name = newName; - 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) @@ -872,7 +874,7 @@ public void RefreshAfterRenameBranch(Models.Branch b, string newName) locals.Add(branch); } - var builder = BuildBranchTree(locals, []); + var builder = BuildBranchTree(locals, [], false); LocalBranchTrees = builder.Locals; RefreshCommits(); @@ -1061,16 +1063,25 @@ public async Task StashAllAsync(bool autoStart) ShowPopup(popup); } + public void NotifyIsSkippingOrAbortingMergeChanged() + { + OnPropertyChanged(nameof(IsSkippingOrAbortingMerge)); + } + public async Task SkipMergeAsync() { - if (_workingCopy != null) - await _workingCopy.SkipMergeAsync(); + if (_workingCopy is not { IsCommitting: false } wc) + return; + + await wc.SkipMergeAsync(); } public async Task AbortMergeAsync() { - if (_workingCopy != null) - await _workingCopy.AbortMergeAsync(); + if (_workingCopy is not { IsCommitting: false } wc) + return; + + await wc.AbortMergeAsync(); } public List<(Models.CustomAction, CustomActionContextMenuLabel)> GetCustomActions(Models.CustomActionScope scope) @@ -1145,6 +1156,8 @@ public void RefreshBranches() LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; + ValidateHistoryFilters(true); + var localBranchesCount = 0; foreach (var b in branches) { @@ -1190,6 +1203,7 @@ public void RefreshTags() Tags = tags; VisibleTags = BuildVisibleTags(); + ValidateHistoryFilters(false); }); }, token); } @@ -1209,10 +1223,24 @@ public void RefreshCommits() var builder = new StringBuilder(); builder .Append('-').Append(Preferences.Instance.MaxHistoryCommits).Append(' ') - .Append(_uiStates.BuildHistoryParams()); + .Append(_uiStates.BuildHistoryParams(GitDir)); - var commits = await new Commands.QueryCommits(FullPath, builder.ToString()).GetResultAsync().ConfigureAwait(false); - var graph = Models.CommitGraph.Parse(commits, _uiStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)); + var commits = await new Commands.QueryCommits(FullPath, builder.ToString()) + .GetResultAsync() + .ConfigureAwait(false); + + var merged = new HashSet(); + foreach (var c in commits) + { + if (merged.Remove(c.SHA)) + c.IsMerged = true; + + if (c.IsMerged) + { + foreach (var p in c.Parents) + merged.Add(p); + } + } Dispatcher.UIThread.Invoke(() => { @@ -1223,8 +1251,6 @@ public void RefreshCommits() { _histories.IsLoading = false; _histories.Commits = commits; - _histories.Graph = graph; - BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToCommitDelayed)) @@ -1307,6 +1333,7 @@ public void RefreshWorkingCopyChanges() Task.Run(async () => { var changes = await new Commands.QueryLocalChanges(FullPath, _uiStates.IncludeUntrackedInLocalChanges, noOptionalLocks) + .WithCancellation(token) .GetResultAsync() .ConfigureAwait(false); @@ -1352,14 +1379,6 @@ public void RefreshStashes() }, token); } - public void ToggleHistoryShowFlag(Models.HistoryShowFlags flag) - { - if (_uiStates.HistoryShowFlags.HasFlag(flag)) - HistoryShowFlags -= flag; - else - HistoryShowFlags |= flag; - } - public void CreateNewBranch() { if (_currentBranch == null) @@ -1402,6 +1421,7 @@ public async Task CheckoutBranchAsync(Models.Branch branch) foreach (var b in _branches) { if (b.IsLocal && + !string.IsNullOrEmpty(b.Upstream) && b.Upstream.Equals(branch.FullName, StringComparison.Ordinal) && b.Ahead.Count == 0) { @@ -1517,7 +1537,7 @@ public async Task AutoUpdateSubmodulesAsync(Models.ICommandLog log) await new Commands.Submodule(FullPath) .Use(log) - .UpdateAsync(submodules); + .UpdateAsync(submodules, false, _settings.EnableRecursiveWhenAutoUpdatingSubmodules, false); } while (false); } @@ -1527,19 +1547,8 @@ public void OpenSubmodule(string submodule) if (selfPage == null) return; - 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); + var fullpath = Path.GetFullPath(Path.Combine(FullPath, submodule)); + App.GetLauncher().OpenSubRepository(selfPage, fullpath); } public void AddWorktree() @@ -1559,16 +1568,8 @@ public void OpenWorktree(Worktree worktree) if (worktree.IsCurrent) return; - var node = Preferences.Instance.FindNode(worktree.FullPath) ?? - new RepositoryNode - { - Id = worktree.FullPath, - Name = Path.GetFileName(worktree.FullPath), - Bookmark = 0, - IsRepository = true, - }; - - App.GetLauncher().OpenRepositoryInTab(node, null); + var normalizedPath = worktree.FullPath.Replace('\\', '/').TrimEnd('/'); + App.GetLauncher().OpenRepositoryInTab(normalizedPath, null); } public async Task LockWorktreeAsync(Worktree worktree) @@ -1613,18 +1614,6 @@ public async Task UnlockWorktreeAsync(Worktree worktree) return all; } - public void DiscardAllChanges() - { - if (CanCreatePopup()) - ShowPopup(new Discard(this)); - } - - public void ClearStashes() - { - if (CanCreatePopup()) - ShowPopup(new ClearStashes(this)); - } - public async Task SaveCommitAsPatchAsync(Models.Commit commit, string folder, int index = 0) { var ignoredChars = new HashSet { '/', '\\', ':', ',', '*', '?', '\"', '<', '>', '|', '`', '$', '^', '%', '[', ']', '+', '-' }; @@ -1673,7 +1662,7 @@ 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(_uiStates.LocalBranchSortMode, _uiStates.RemoteBranchSortMode); if (string.IsNullOrEmpty(_filter)) @@ -1681,8 +1670,11 @@ private BranchTreeNode.Builder BuildBranchTree(List branches, Lis builder.SetExpandedNodes(_uiStates.ExpandedBranchNodesInSideBar); builder.Run(branches, remotes, false); - foreach (var invalid in builder.InvalidExpandedNodes) - _uiStates.ExpandedBranchNodesInSideBar.Remove(invalid); + if (validateExpandedNodes) + { + foreach (var invalid in builder.InvalidExpandedNodes) + _uiStates.ExpandedBranchNodesInSideBar.Remove(invalid); + } } else { @@ -1832,6 +1824,37 @@ private void ResetTagFilterMode() } } + private void ValidateHistoryFilters(bool forBranch) + { + if (_historyFilterMode == Models.FilterMode.None) + return; + + var set = new HashSet(); + + if (forBranch) + { + foreach (var b in _branches) + set.Add(b.FullName); + + foreach (var f in _uiStates.HistoryFilters) + { + if (f.Type is Models.FilterType.LocalBranch or Models.FilterType.RemoteBranch) + f.IsValid = set.Contains(f.Pattern); + } + } + else + { + foreach (var t in _tags) + set.Add(t.Name); + + foreach (var f in _uiStates.HistoryFilters) + { + if (f.Type is Models.FilterType.Tag) + f.IsValid = set.Contains(f.Pattern); + } + } + } + private BranchTreeNode FindBranchNode(List nodes, string path) { if (string.IsNullOrEmpty(path)) @@ -1889,11 +1912,11 @@ private async Task AutoFetchOnUIThread() if (desire > now) return; - var remotes = new List(); + var remotes = new List(); foreach (var r in _remotes) { if (!r.DisableAutoFetch) - remotes.Add(r.Name); + remotes.Add(r); } if (remotes.Count == 0) @@ -1903,16 +1926,16 @@ private async Task AutoFetchOnUIThread() log = CreateLog("Auto-Fetch"); foreach (var remote in remotes) - await new Commands.Fetch(FullPath, remote).Use(log).RunAsync(); + await new Commands.Fetch(FullPath, remote).Use(log).ExecAsync(); _lastFetchTime = DateTime.Now; - IsAutoFetching = false; } catch { // Ignore all exceptions. } + IsAutoFetching = false; log?.Complete(); } @@ -1933,9 +1956,6 @@ private async Task AutoFetchOnUIThread() private int _localChangesCount = 0; private int _stashesCount = 0; - private bool _isSearchingCommits = false; - private SearchCommitContext _searchCommitContext = null; - private string _filter = string.Empty; private List _remotes = []; private List _branches = []; diff --git a/src/ViewModels/RepositoryCommandPalette.cs b/src/ViewModels/RepositoryCommandPalette.cs index 2b57414a7b..796629e373 100644 --- a/src/ViewModels/RepositoryCommandPalette.cs +++ b/src/ViewModels/RepositoryCommandPalette.cs @@ -59,7 +59,7 @@ public RepositoryCommandPalette(Repository repo) // 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.WithHead", "compare", "Compare", new CompareCommandPalette(repo, null))); + _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))); @@ -72,7 +72,8 @@ public RepositoryCommandPalette(Repository repo) _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", "Diff", () => repo.ApplyPatch())); + _cmds.Add(new("Apply.Title", "apply", "ApplyPatch", () => repo.ApplyPatch())); + _cmds.Add(new("Repository.DiscardAll", "discard", "Undo", () => repo.WorkingCopy.DiscardAllChanges())); _cmds.Sort((l, r) => l.Label.CompareTo(r.Label)); _visibleCmds = _cmds; diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index 06176f4375..a761947169 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -101,6 +101,12 @@ public bool AskBeforeAutoUpdatingSubmodules set => _repo.Settings.AskBeforeAutoUpdatingSubmodules = value; } + public bool EnableRecursiveWhenAutoUpdatingSubmodules + { + get => _repo.Settings.EnableRecursiveWhenAutoUpdatingSubmodules; + set => _repo.Settings.EnableRecursiveWhenAutoUpdatingSubmodules = value; + } + public AvaloniaList CommitTemplates { get => _repo.Settings.CommitTemplates; diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index 1f5515b9de..9d7ab9debc 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/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index fc68dc666e..5f0e2fc1f3 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -65,22 +65,17 @@ public List VisibleChanges private set => SetProperty(ref _visibleChanges, value); } - public List SelectedChanges + public ChangeSelection ChangeSelection { - get => _selectedChanges; + get => _changeSelection; set { - if (SetProperty(ref _selectedChanges, value)) + if (SetProperty(ref _changeSelection, value)) { - if (value is { Count: 1 }) - { - var option = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), value[0]); - DiffContext = new DiffContext(_repo.FullPath, option, _diffContext); - } + if (value is { Count: 1, HasFolder: false }) + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), value.Changes[0]), _diffContext); else - { DiffContext = null; - } } } } @@ -104,11 +99,16 @@ public DiffContext DiffContext public RevisionCompare(Repository repo, Models.Commit startPoint, Models.Commit endPoint) { _repo = repo; - _startPoint = (object)startPoint ?? new Models.Null(); - _endPoint = (object)endPoint ?? new Models.Null(); + _startPoint = (object)startPoint ?? Models.Null.Instance; + _endPoint = (object)endPoint ?? Models.Null.Instance; Refresh(); } + public RevisionCompare Clone() + { + 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(); @@ -141,7 +141,7 @@ public void Swap() { (StartPoint, EndPoint) = (_endPoint, _startPoint); VisibleChanges = []; - SelectedChanges = []; + ChangeSelection = new(null); IsLoading = true; Refresh(); } @@ -367,9 +367,9 @@ private void Refresh() IsLoading = false; if (VisibleChanges.Count > 0) - SelectedChanges = [VisibleChanges[0]]; + ChangeSelection = new(VisibleChanges.GetRange(0, 1)); else - SelectedChanges = []; + ChangeSelection = new(null); }); }); } @@ -391,7 +391,7 @@ private string GetDesc(object obj) private int _totalChanges = 0; private List _changes = null; private List _visibleChanges = null; - private List _selectedChanges = null; + private ChangeSelection _changeSelection = new(null); private string _searchFilter = string.Empty; private DiffContext _diffContext = null; } diff --git a/src/ViewModels/Reword.cs b/src/ViewModels/Reword.cs deleted file mode 100644 index edae3711e0..0000000000 --- a/src/ViewModels/Reword.cs +++ /dev/null @@ -1,93 +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.UIStates.EnableSignOffForCommit; - var noVerify = _repo.UIStates.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", false); - if (!succ) - { - log.Complete(); - return false; - } - } - - succ = await new Commands.Commit(_repo.FullPath, _message, signOff, noVerify, true, false) - .Use(log) - .RunAsync(); - - if (succ) - { - if (needAutoStash) - await new Commands.Stash(_repo.FullPath) - .Use(log) - .PopAsync("stash@{0}"); - - if (_repo.SelectedViewIndex == 0) - { - var head = await new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResultAsync(); - _repo.NavigateToCommit(head, true); - } - } - - log.Complete(); - return succ; - } - - private readonly Repository _repo; - private readonly string _oldMessage; - private string _message; - } -} diff --git a/src/ViewModels/SSHKeyGenerator.cs b/src/ViewModels/SSHKeyGenerator.cs new file mode 100644 index 0000000000..00986e4474 --- /dev/null +++ b/src/ViewModels/SSHKeyGenerator.cs @@ -0,0 +1,161 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public partial class SSHKeyGenerator : ObservableValidator + { + public Models.SSHKeyType Type + { + get => _type; + set => SetProperty(ref _type, value); + } + + [Required(ErrorMessage = "Name is required.")] + [RegularExpression(@"^[a-zA-Z0-9_\-]+$", ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")] + public string Name + { + get => _name; + set => SetProperty(ref _name, value, true); + } + + [Required(ErrorMessage = "Email is required.")] + [EmailAddress(ErrorMessage = "Invalid email address.")] + public string Email + { + get => _email; + set => SetProperty(ref _email, value, true); + } + + public bool UsePassphrase + { + get => _usePassphrase; + set + { + if (SetProperty(ref _usePassphrase, value)) + { + ValidateProperty(_passphrase, nameof(Passphrase)); + ValidateProperty(_confirmedPassphrase, nameof(ConfirmedPassphrase)); + } + } + } + + [CustomValidation(typeof(SSHKeyGenerator), nameof(ValidatePassphrase))] + public string Passphrase + { + get => _passphrase; + set + { + if (SetProperty(ref _passphrase, value, true)) + ValidateProperty(_confirmedPassphrase, nameof(ConfirmedPassphrase)); + } + } + + [CustomValidation(typeof(SSHKeyGenerator), nameof(ValidateConfirmedPassphrase))] + public string ConfirmedPassphrase + { + get => _confirmedPassphrase; + set => SetProperty(ref _confirmedPassphrase, value, true); + } + + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + + public static ValidationResult ValidatePassphrase(string password, ValidationContext context) + { + var instance = (SSHKeyGenerator)context.ObjectInstance; + if (!instance.UsePassphrase) + return ValidationResult.Success; + + if (string.IsNullOrEmpty(password)) + return new ValidationResult("Passphrase is required!"); + + if (!REG_PSWD_FORMAT().IsMatch(password)) + return new ValidationResult("Passphrase can only contain letters, numbers, and special characters: _ + - = @ # $ % ! &"); + + return ValidationResult.Success; + } + + public static ValidationResult ValidateConfirmedPassphrase(string confirmedPassword, ValidationContext context) + { + var instance = (SSHKeyGenerator)context.ObjectInstance; + if (!instance.UsePassphrase) + return ValidationResult.Success; + + if (confirmedPassword != instance.Passphrase) + return new ValidationResult("Passphrase and confirmation do not match!"); + + return ValidationResult.Success; + } + + public Models.SSHKeyPair Run() + { + ErrorMessage = string.Empty; + + ValidateAllProperties(); + if (HasErrors) + return null; + + var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh"); + if (!Directory.Exists(dir)) + { + try + { + Directory.CreateDirectory(dir); + } + catch (Exception e) + { + ErrorMessage = $"Failed to create .ssh directory: {e.Message}"; + return null; + } + } + + var type = _type?.Cmdline ?? "-t ed25519"; + var passphrase = _usePassphrase ? _passphrase : string.Empty; + var keyFile = Path.Combine(dir, _name); + var start = new ProcessStartInfo(); + start.FileName = "ssh-keygen"; + start.Arguments = $"-q {type} -N {passphrase.Quoted()} -C {_email.Quoted()} -f {keyFile.Quoted()}"; + start.UseShellExecute = false; + start.CreateNoWindow = true; + + try + { + var proc = Process.Start(start); + proc.WaitForExit(); + proc.Close(); + } + catch (Exception e) + { + ErrorMessage = $"Failed to generate SSH key: {e.Message}"; + return null; + } + + var publicKeyFile = keyFile + ".pub"; + if (File.Exists(keyFile) && File.Exists(publicKeyFile)) + return new Models.SSHKeyPair(keyFile, publicKeyFile); + + ErrorMessage = "Failed to generate SSH key: Key files not found."; + return null; + } + + [GeneratedRegex(@"^[0-9a-zA-Z_\-\@\#\$\%\!\&\+\=]+$")] + private static partial Regex REG_PSWD_FORMAT(); + + private Models.SSHKeyType _type = Models.SSHKeyType.Supported[0]; + private string _name; + private string _email; + private bool _usePassphrase; + private string _passphrase; + private string _confirmedPassphrase; + private string _errorMessage; + } +} diff --git a/src/ViewModels/SSHKeyHelper.cs b/src/ViewModels/SSHKeyHelper.cs new file mode 100644 index 0000000000..0b4c9de31b --- /dev/null +++ b/src/ViewModels/SSHKeyHelper.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class SSHKeyHelper : ObservableObject + { + public AvaloniaList Keys + { + get; + } + + public Models.SSHKeyPair SelectedKey + { + get => _selectedKey; + set => SetProperty(ref _selectedKey, value); + } + + public SSHKeyGenerator Generator + { + get => _generator; + private set => SetProperty(ref _generator, value); + } + + public SSHKeyHelper() + { + Keys = new AvaloniaList(); + + var sshDir = new DirectoryInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh")); + if (sshDir.Exists) + { + var files = sshDir.GetFiles("*.pub"); + var keys = new List(); + + foreach (var file in files) + { + var privateKeyPath = file.FullName.Substring(0, file.FullName.Length - 4); + if (File.Exists(privateKeyPath)) + keys.Add(new(privateKeyPath, file.FullName)); + } + + if (keys.Count > 0) + { + keys.Sort((l, r) => l.Name.CompareTo(r.Name)); + Keys.AddRange(keys); + SelectedKey = keys[0]; + } + } + } + + public void OpenGenerator() + { + Generator = new SSHKeyGenerator(); + } + + public void CloseGenerator() + { + Generator = null; + } + + public void Generate() + { + var key = _generator.Run(); + if (key == null) + return; + + Keys.Add(key); + SelectedKey = key; + Generator = null; + } + + public void DeleteSelected() + { + var key = SelectedKey; + if (key == null) + return; + + try + { + if (File.Exists(key.PrivateKeyPath)) + File.Delete(key.PrivateKeyPath); + if (File.Exists(key.PublicKeyPath)) + File.Delete(key.PublicKeyPath); + + var idx = Keys.IndexOf(key); + if (idx >= 0) + { + Keys.RemoveAt(idx); + + if (Keys.Count == 0) + SelectedKey = null; + else if (idx > Keys.Count - 1) + SelectedKey = Keys[Keys.Count - 1]; + else + SelectedKey = Keys[idx]; + } + } + catch + { + // Ignore any errors during deletion + } + } + + private Models.SSHKeyPair _selectedKey = null; + private SSHKeyGenerator _generator = null; + } +} diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 0a5bf34381..693c4ca415 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -93,8 +93,10 @@ 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.OrdinalIgnoreCase)) { @@ -104,7 +106,7 @@ public override async Task Sure() else if (parent.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) { var relative = parent.Substring(normalizedRoot.Length).TrimStart('/'); - var group = FindOrCreateGroupRecursive(Preferences.Instance.RepositoryNodes, relative); + var group = Preferences.Instance.FindOrCreateGroupRecursive(relative); var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(f, group, false, false); await node.UpdateStatusAsync(false, null); } @@ -113,6 +115,10 @@ public override async Task Sure() Preferences.Instance.AutoRemoveInvalidNode(); Preferences.Instance.Save(); Welcome.Instance.Refresh(); + + if (_untrusted > 0) + Models.Notification.Send(null, App.Text("TrustRepository.ScanSkipped", _untrusted)); + return true; } @@ -152,6 +158,10 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List if (!IsManaged(normalized)) outs.Add(normalized); } + else if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr)) + { + _untrusted++; + } continue; } @@ -168,39 +178,6 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List } } - private RepositoryNode FindOrCreateGroupRecursive(List collection, string path) - { - RepositoryNode node = null; - foreach (var name in path.Split('/')) - { - node = FindOrCreateGroup(collection, name); - collection = node.SubNodes; - } - - return node; - } - - private RepositoryNode FindOrCreateGroup(List collection, string name) - { - foreach (var node in collection) - { - if (node.Name.Equals(name, StringComparison.Ordinal)) - return node; - } - - var added = new RepositoryNode() - { - Id = Guid.NewGuid().ToString(), - Name = name, - IsRepository = false, - IsExpanded = true, - }; - collection.Add(added); - - Preferences.Instance.SortNodes(collection); - return added; - } - private bool IsManaged(string path) { if (OperatingSystem.IsLinux()) @@ -213,5 +190,6 @@ private bool IsManaged(string path) private bool _useCustomDir = false; private string _customDir = string.Empty; private Models.ScanDir _selected = null; + private int _untrusted = 0; } } diff --git a/src/ViewModels/SearchCommitContext.cs b/src/ViewModels/SearchCommitContext.cs index 2ec14519dc..f7885e809e 100644 --- a/src/ViewModels/SearchCommitContext.cs +++ b/src/ViewModels/SearchCommitContext.cs @@ -42,7 +42,7 @@ public bool OnlySearchCurrentBranch } } - public List Suggestions + public List Suggestions { get => _suggestions; private set => SetProperty(ref _suggestions, value); @@ -66,13 +66,14 @@ public Models.Commit Selected set { if (SetProperty(ref _selected, value) && value != null) - _repo.NavigateToCommit(value.SHA); + _histories.NavigateTo(value.SHA); } } - public SearchCommitContext(Repository repo) + public SearchCommitContext(string repo, Histories histories) { _repo = repo; + _histories = histories; } public void ClearFilter() @@ -108,21 +109,20 @@ public void StartSearch() { var result = new List(); var method = (Models.CommitSearchMethod)_method; - var repoPath = _repo.FullPath; if (method == Models.CommitSearchMethod.BySHA) { - var isCommitSHA = await new Commands.IsCommitSHA(repoPath, _filter) + var isCommitSHA = await new Commands.IsCommitSHA(_repo, _filter) .GetResultAsync() .ConfigureAwait(false); if (isCommitSHA) { - var commit = await new Commands.QuerySingleCommit(repoPath, _filter) + var commit = await new Commands.QuerySingleCommit(_repo, _filter) .GetResultAsync() .ConfigureAwait(false); - commit.IsMerged = await new Commands.IsAncestor(repoPath, commit.SHA, "HEAD") + commit.IsMerged = await new Commands.IsAncestor(_repo, commit.SHA, "HEAD") .GetResultAsync() .ConfigureAwait(false); @@ -131,7 +131,7 @@ public void StartSearch() } else if (_onlySearchCurrentBranch) { - result = await new Commands.QueryCommits(repoPath, _filter, method, true) + result = await new Commands.QueryCommits(_repo, _filter, method, true) .GetResultAsync() .ConfigureAwait(false); @@ -140,13 +140,13 @@ public void StartSearch() } else { - result = await new Commands.QueryCommits(repoPath, _filter, method, false) + result = await new Commands.QueryCommits(_repo, _filter, method, false) .GetResultAsync() .ConfigureAwait(false); if (result.Count > 0) { - var set = await new Commands.QueryCurrentBranchCommitHashes(repoPath, result[^1].CommitterTime) + var set = await new Commands.QueryCurrentBranchCommitHashes(_repo, result[^1].CommitterTime) .GetResultAsync() .ConfigureAwait(false); @@ -161,8 +161,12 @@ public void StartSearch() return; IsQuerying = false; - if (_repo.IsSearchingCommits) + if (_histories.IsSearchingCommits) + { Results = result; + if (method == Models.CommitSearchMethod.BySHA && result.Count == 1) + Selected = result[0]; + } }); }, token); } @@ -173,6 +177,8 @@ public void EndSearch() _cancellation.Cancel(); _worktreeFiles = null; + _authors = null; + IsQuerying = false; Suggestions = null; Results = null; @@ -181,67 +187,117 @@ 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) + .GetResultAsync() + .ConfigureAwait(false); - Task.Run(async () => + Dispatcher.UIThread.Post(() => + { + _requestingAuthors = false; + + if (_histories.IsSearchingCommits) + { + _authors = authors; + UpdateSuggestions(); + } + }); + }); + + return; + } + + if (_authors.Count == 0 || _filter.Length < 2) { - var files = await new Commands.QueryRevisionFileNames(_repo.FullPath, "HEAD") - .GetResultAsync() - .ConfigureAwait(false); + Suggestions = null; + return; + } + + 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); + } - Dispatcher.UIThread.Post(() => + 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, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); - if (_repo.IsSearchingCommits) + Dispatcher.UIThread.Post(() => { - _worktreeFiles = files; - UpdateSuggestions(); - } + _requestingWorktreeFiles = false; + + if (_histories.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 string _repo = null; + private Histories _histories = 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/SetPushURL.cs b/src/ViewModels/SetPushURL.cs new file mode 100644 index 0000000000..dd5606f935 --- /dev/null +++ b/src/ViewModels/SetPushURL.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class SetPushURL : Popup + { + public Models.Remote Remote + { + get => _remote; + } + + [CustomValidation(typeof(SetPushURL), nameof(ValidatePushURL))] + public string PushURL + { + get => _pushURL; + set => SetProperty(ref _pushURL, value, true); + } + + public SetPushURL(Repository repo, Models.Remote remote) + { + _repo = repo; + _remote = remote; + _pushURL = string.IsNullOrEmpty(remote.PushURL) ? remote.URL : remote.PushURL; + } + + public static ValidationResult ValidatePushURL(string url, ValidationContext ctx) + { + if (ctx.ObjectInstance is SetPushURL set) + { + if (!string.IsNullOrEmpty(url) && !Models.Remote.IsValidURL(url)) + return new ValidationResult("Bad remote URL format!!!"); + } + + return ValidationResult.Success; + } + + public override async Task Sure() + { + if (_remote.PushURL == _pushURL) + return true; + + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Set PushURL for '{_remote.Name}' ..."; + + var succ = false; + if (string.IsNullOrEmpty(_pushURL)) + { + succ = await new Commands.Remote(_repo.FullPath) + .SetURLAsync(_remote.Name, _remote.PushURL, true, true); + } + else if (_pushURL.Equals(_remote.URL, StringComparison.Ordinal)) + { + if (!string.IsNullOrEmpty(_remote.PushURL)) + succ = await new Commands.Remote(_repo.FullPath).SetURLAsync(_remote.Name, _remote.PushURL, true, true); + else + succ = true; + + _pushURL = null; + } + else + { + succ = await new Commands.Remote(_repo.FullPath) + .SetURLAsync(_remote.Name, _pushURL, false, true); + } + + if (succ) + _remote.PushURL = _pushURL; + + return succ; + } + + private readonly Repository _repo; + private readonly Models.Remote _remote; + private string _pushURL; + } +} diff --git a/src/ViewModels/SquashOrFixupHead.cs b/src/ViewModels/SquashOrFixupHead.cs deleted file mode 100644 index dc640d04ed..0000000000 --- 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.UIStates.EnableSignOffForCommit; - var noVerify = _repo.UIStates.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", false); - 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/StashesPage.cs b/src/ViewModels/StashesPage.cs index 9f4874dba6..055762fb6c 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -93,23 +93,23 @@ public List Changes private set { if (SetProperty(ref _changes, value)) - SelectedChanges = value is { Count: > 0 } ? [value[0]] : []; + ChangeSelection = new(value is { Count: > 0 } ? value.GetRange(0, 1) : null); } } - public List SelectedChanges + public ChangeSelection ChangeSelection { - get => _selectedChanges; + get => _changeSelection; set { - if (SetProperty(ref _selectedChanges, value)) + if (SetProperty(ref _changeSelection, value)) { - if (value is not { Count: 1 }) + if (value is not { Count: 1, HasFolder: false }) DiffContext = null; - else if (_untracked.Contains(value[0])) - DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], value[0]), _diffContext); + else if (_untracked.Contains(value.Changes[0])) + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], value.Changes[0]), _diffContext); else - DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, value[0]), _diffContext); + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, value.Changes[0]), _diffContext); } } } @@ -125,6 +125,12 @@ public StashesPage(Repository repo) _repo = repo; } + public void ClearStashes() + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new ClearStashes(_repo)); + } + public void ClearSearchFilter() { SearchFilter = string.Empty; @@ -281,7 +287,7 @@ private void RefreshVisible() private Models.Stash _selectedStash = null; private List _changes = null; private List _untracked = []; - private List _selectedChanges = []; + private ChangeSelection _changeSelection = new(null); private DiffContext _diffContext = null; } } diff --git a/src/ViewModels/Statistics.cs b/src/ViewModels/Statistics.cs index a16c94ecc6..9d7f620bdb 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,12 +13,28 @@ public bool IsLoading private set => SetProperty(ref _isLoading, value); } - public int SelectedIndex + public List Branches + { + get => _branches; + private set => SetProperty(ref _branches, value); + } + + public Models.Branch SelectedBranch { - get => _selectedIndex; + get => _selectedBranch; set { - if (SetProperty(ref _selectedIndex, value)) + if (SetProperty(ref _selectedBranch, value)) + LoadStatistics(); + } + } + + public Models.StatisticsMode ViewMode + { + get => _viewMode; + set + { + if (SetProperty(ref _viewMode, value)) RefreshReport(); } } @@ -28,37 +42,59 @@ public int SelectedIndex public Models.StatisticsReport SelectedReport { get => _selectedReport; - private set - { - value?.ChangeAuthor(null); - SetProperty(ref _selectedReport, value); - } + private set => SetProperty(ref _selectedReport, value); } - public uint SampleColor + public Models.StatisticsAuthor SelectedAuthor { - get => Preferences.Instance.StatisticsSampleColor; - set - { - if (value != Preferences.Instance.StatisticsSampleColor) - { - Preferences.Instance.StatisticsSampleColor = value; - OnPropertyChanged(nameof(SampleBrush)); - _selectedReport?.ChangeColor(value); - } - } + get => _selectedAuthor; } - public IBrush SampleBrush + 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 || author == _selectedAuthor) + return; + + _selectedAuthor = author; + Samples = SelectedReport.GetSamples(author); + } + + private void LoadBranches() { Task.Run(async () => { - var result = await new Commands.Statistics(repo, Preferences.Instance.MaxHistoryCommits).ReadAsync().ConfigureAwait(false); + 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, 200000, _selectedBranch) + .ReadAsync() + .ConfigureAwait(false); + Dispatcher.UIThread.Post(() => { _data = result; @@ -73,20 +109,24 @@ 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.StatisticsAuthor _selectedAuthor = null; + private Models.StatisticsSamples _samples = null; } } diff --git a/src/ViewModels/SubmoduleRevisionCompare.cs b/src/ViewModels/SubmoduleRevisionCompare.cs index bbf41a2dbc..5a975f5ea4 100644 --- a/src/ViewModels/SubmoduleRevisionCompare.cs +++ b/src/ViewModels/SubmoduleRevisionCompare.cs @@ -38,15 +38,15 @@ public List VisibleChanges private set => SetProperty(ref _visibleChanges, value); } - public List SelectedChanges + public ChangeSelection ChangeSelection { - get => _selectedChanges; + get => _changeSelection; set { - if (SetProperty(ref _selectedChanges, value)) + if (SetProperty(ref _changeSelection, value)) { - if (value is { Count: 1 }) - DiffContext = new DiffContext(_repo, new Models.DiffOption(_base.SHA, _to.SHA, value[0]), _diffContext); + if (value is { Count: 1, HasFolder: false }) + DiffContext = new DiffContext(_repo, new Models.DiffOption(_base.SHA, _to.SHA, value.Changes[0]), _diffContext); else DiffContext = null; } @@ -108,7 +108,7 @@ private void Refresh() { IsLoading = true; VisibleChanges = []; - SelectedChanges = []; + ChangeSelection = new(null); Task.Run(async () => { @@ -134,9 +134,7 @@ private void Refresh() IsLoading = false; if (VisibleChanges.Count > 0) - SelectedChanges = [VisibleChanges[0]]; - else - SelectedChanges = []; + ChangeSelection = new(VisibleChanges.GetRange(0, 1)); }); }); } @@ -170,7 +168,7 @@ private void RefreshVisible() private int _totalChanges = 0; private List _changes = null; private List _visibleChanges = null; - private List _selectedChanges = null; + private ChangeSelection _changeSelection = new(null); private string _searchFilter = string.Empty; private DiffContext _diffContext = null; } diff --git a/src/ViewModels/TrustRepository.cs b/src/ViewModels/TrustRepository.cs new file mode 100644 index 0000000000..96615b209d --- /dev/null +++ b/src/ViewModels/TrustRepository.cs @@ -0,0 +1,105 @@ +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class TrustRepository : Popup + { + public string TargetPath + { + get; + } + + public string SafeDirectory + { + get; + } + + public string Command + { + get; + } + + public string Reason + { + get; + } + + public TrustRepository(string pageId, string path, string reason, string safeDirectory, RepositoryNode parent, bool moveNode, bool open, int bookmark) + { + _pageId = pageId; + _parent = parent; + _moveNode = moveNode; + _open = open; + _bookmark = bookmark; + + TargetPath = path; + SafeDirectory = safeDirectory; + Command = App.Text("TrustRepository.CommandTip", safeDirectory); + Reason = GetReason(reason); + } + + public override async Task Sure() + { + var log = new CommandLog("Trust Repository"); + Use(log); + + ProgressDescription = $"Adding '{SafeDirectory}' into git global `safe.directory` ..."; + var added = await new Commands.AddSafeDirectory(_pageId, SafeDirectory).Use(log).ExecAsync(); + if (!added) + { + log.Complete(); + return false; + } + + ProgressDescription = $"Opening '{TargetPath}' ..."; + + var root = TargetPath; + var isBare = await new Commands.IsBareRepository(TargetPath).GetResultAsync(); + if (!isBare) + { + var test = await new Commands.QueryRepositoryRootPath(TargetPath).GetResultAsync(); + if (!test.IsSuccess || string.IsNullOrWhiteSpace(test.StdOut)) + { + log.Complete(); + Models.Notification.Send(_pageId, string.IsNullOrWhiteSpace(test.StdErr) ? "Failed to open the repository after trusting it." : test.StdErr, true); + return false; + } + + root = test.StdOut.Trim(); + } + + log.Complete(); + + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(root, _parent, _moveNode); + node.Bookmark = _bookmark; + await node.UpdateStatusAsync(false, null); + Welcome.Instance.Refresh(); + + if (_open) + node.Open(); + + return true; + } + + private static string GetReason(string stderr) + { + if (string.IsNullOrWhiteSpace(stderr)) + return string.Empty; + + // Only the first line (e.g. "fatal: detected dubious ownership in repository at '...'") is shown. + // The remaining lines only contain the owner SIDs and git's suggested `git config` command. + var line = stderr.Trim(); + var idx = line.IndexOf('\n'); + if (idx >= 0) + line = line.Substring(0, idx); + + return line.Trim(); + } + + private readonly string _pageId; + private readonly RepositoryNode _parent; + private readonly bool _moveNode; + private readonly bool _open; + private readonly int _bookmark; + } +} diff --git a/src/ViewModels/UpdateSubmodules.cs b/src/ViewModels/UpdateSubmodules.cs index af55aba9dc..323a60b2db 100644 --- a/src/ViewModels/UpdateSubmodules.cs +++ b/src/ViewModels/UpdateSubmodules.cs @@ -39,6 +39,12 @@ public bool EnableInit set; } = true; + public bool EnableRecursive + { + get; + set; + } = true; + public bool EnableRemote { get; @@ -63,6 +69,8 @@ public UpdateSubmodules(Repository repo, Models.Submodule selected) IsEnableInitVisible = true; HasPreSelectedSubmodule = false; } + + EnableRecursive = _repo.Settings.EnableRecursiveWhenAutoUpdatingSubmodules; } public override async Task Sure() @@ -87,7 +95,7 @@ public override async Task Sure() await new Commands.Submodule(_repo.FullPath) .Use(log) - .UpdateAsync(targets, EnableInit, EnableRemote); + .UpdateAsync(targets, EnableInit, EnableRecursive, EnableRemote); log.Complete(); _repo.MarkSubmodulesDirtyManually(); diff --git a/src/ViewModels/ViewLogs.cs b/src/ViewModels/ViewLogs.cs index 8dc3e96050..21ab81ab73 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 49539292d7..58707d8a98 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; @@ -100,32 +99,16 @@ public void ToggleNodeIsExpanded(RepositoryNode node) } } - public async Task GetRepositoryRootAsync(string path) + public async Task GetRepositoryRootAsync(string path) { if (!Preferences.Instance.IsGitConfigured()) - { - Models.Notification.Send(null, App.Text("NotConfigured"), true); - return null; - } - - var root = path; - if (!Directory.Exists(root)) - { - if (File.Exists(root)) - root = Path.GetDirectoryName(root); - else - return null; - } + return Commands.Command.Result.Failed(App.Text("NotConfigured")); - var isBare = await new Commands.IsBareRepository(root).GetResultAsync(); + var isBare = await new Commands.IsBareRepository(path).GetResultAsync(); if (isBare) - return root; - - var rs = await new Commands.QueryRepositoryRootPath(root).GetResultAsync(); - if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) - return null; + return new Commands.Command.Result { IsSuccess = true, StdOut = path }; - return rs.StdOut.Trim(); + return await new Commands.QueryRepositoryRootPath(path).GetResultAsync(); } public async Task AddRepositoryAsync(string path, RepositoryNode parent, bool moveNode, bool open) diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 930b375859..38612dbdaa 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -65,7 +65,11 @@ public bool IsUnstaging public bool IsCommitting { get => _isCommitting; - private set => SetProperty(ref _isCommitting, value); + private set + { + if (SetProperty(ref _isCommitting, value)) + _repo.NotifyIsSkippingOrAbortingMergeChanged(); + } } public bool EnableSignOff @@ -108,7 +112,7 @@ public bool UseAmend Staged = GetStagedChanges(_cached); VisibleStaged = GetVisibleChanges(_staged); - SelectedStaged = []; + SelectedStaged = new(null); } } } @@ -131,7 +135,8 @@ public string Filter VisibleUnstaged = GetVisibleChanges(_unstaged); VisibleStaged = GetVisibleChanges(_staged); - SelectedUnstaged = []; + SelectedUnstaged = new(null); + SelectedStaged = new(null); } } } @@ -139,75 +144,91 @@ public string Filter public List Unstaged { get => _unstaged; - private set => SetProperty(ref _unstaged, value); + private set + { + if (SetProperty(ref _unstaged, value)) + OnPropertyChanged(nameof(UnstagedCountInfo)); + } } public List VisibleUnstaged { get => _visibleUnstaged; - private set => SetProperty(ref _visibleUnstaged, value); + private set + { + if (SetProperty(ref _visibleUnstaged, value)) + OnPropertyChanged(nameof(UnstagedCountInfo)); + } } public List Staged { get => _staged; - private set => SetProperty(ref _staged, value); + private set + { + if (SetProperty(ref _staged, value)) + OnPropertyChanged(nameof(StagedCountInfo)); + } } public List VisibleStaged { get => _visibleStaged; - private set => SetProperty(ref _visibleStaged, value); + private set + { + if (SetProperty(ref _visibleStaged, value)) + OnPropertyChanged(nameof(StagedCountInfo)); + } } - public List SelectedUnstaged + public string UnstagedCountInfo + { + get => string.IsNullOrEmpty(_filter) ? $"({_unstaged.Count})" : $"({_visibleUnstaged.Count}/{_unstaged.Count})"; + } + + public string StagedCountInfo + { + get => string.IsNullOrEmpty(_filter) ? $"({_staged.Count})" : $"({_visibleStaged.Count}/{_staged.Count})"; + } + + public ChangeSelection SelectedUnstaged { get => _selectedUnstaged; set { if (SetProperty(ref _selectedUnstaged, value)) { - if (value == null || value.Count == 0) - { - if (_selectedStaged == null || _selectedStaged.Count == 0) - SetDetail(null, true); - } - else + if (value is { Count: > 0 }) { + _isLoadingData = true; if (_selectedStaged is { Count: > 0 }) - SelectedStaged = []; - - if (value.Count == 1) - SetDetail(value[0], true); - else - SetDetail(null, true); + SelectedStaged = new(null); + _isLoadingData = false; } + + if (!_isLoadingData) + UpdateDetail(); } } } - public List SelectedStaged + public ChangeSelection SelectedStaged { get => _selectedStaged; set { if (SetProperty(ref _selectedStaged, value)) { - if (value == null || value.Count == 0) - { - if (_selectedUnstaged == null || _selectedUnstaged.Count == 0) - SetDetail(null, false); - } - else + if (value is { Count: > 0 }) { + _isLoadingData = true; if (_selectedUnstaged is { Count: > 0 }) - SelectedUnstaged = []; - - if (value.Count == 1) - SetDetail(value[0], false); - else - SetDetail(null, false); + SelectedUnstaged = new(null); + _isLoadingData = false; } + + if (!_isLoadingData) + UpdateDetail(); } } } @@ -233,6 +254,35 @@ public void SetData(List changes) { if (!IsChanged(_cached, changes)) { + if (_useAmend) + { + var testStaged = GetStagedChanges(_cached); + if (IsChanged(_staged, testStaged)) + { + var visibleStagedNew = GetVisibleChanges(testStaged); + var selectedStagedNew = new List(); + + if (_selectedStaged is { Count: > 0 }) + { + var set = new HashSet(); + foreach (var c in _selectedStaged.Changes) + set.Add(c.Path); + + foreach (var c in visibleStagedNew) + { + if (set.Contains(c.Path)) + selectedStagedNew.Add(c); + } + } + + _isLoadingData = true; + Staged = testStaged; + VisibleStaged = visibleStagedNew; + SelectedStaged = new(selectedStagedNew); + _isLoadingData = false; + } + } + HasUnsolvedConflicts = _cached.Find(x => x.IsConflicted) != null; UpdateInProgressState(); UpdateDetail(); @@ -242,7 +292,7 @@ public void SetData(List changes) var lastSelectedUnstaged = new HashSet(); if (_selectedUnstaged is { Count: > 0 }) { - foreach (var c in _selectedUnstaged) + foreach (var c in _selectedUnstaged.Changes) lastSelectedUnstaged.Add(c.Path); } @@ -250,14 +300,16 @@ public void SetData(List changes) var visibleUnstaged = new List(); var selectedUnstaged = new List(); var noFilter = string.IsNullOrEmpty(_filter); - var hasConflict = false; + var conflicts = new List(); var canSwitchDirectly = true; foreach (var c in changes) { if (c.WorkTree != Models.ChangeState.None) { unstaged.Add(c); - hasConflict |= c.IsConflicted; + + if (c.IsConflicted) + conflicts.Add(c); if (noFilter || c.Path.Contains(_filter, StringComparison.OrdinalIgnoreCase)) { @@ -282,7 +334,7 @@ public void SetData(List changes) if (_selectedStaged is { Count: > 0 }) { var set = new HashSet(); - foreach (var c in _selectedStaged) + foreach (var c in _selectedStaged.Changes) set.Add(c.Path); foreach (var c in visibleStaged) @@ -292,22 +344,22 @@ public void SetData(List changes) } } - if (selectedUnstaged.Count == 0 && selectedStaged.Count == 0 && hasConflict) + if (selectedUnstaged.Count == 0 && selectedStaged.Count == 0 && conflicts.Count > 0) { - var firstConflict = visibleUnstaged.Find(x => x.IsConflicted); + var firstConflict = FindFirstConflict(conflicts); selectedUnstaged.Add(firstConflict); } _isLoadingData = true; _cached = changes; - HasUnsolvedConflicts = hasConflict; + HasUnsolvedConflicts = conflicts.Count > 0; CanSwitchBranchDirectly = canSwitchDirectly; VisibleUnstaged = visibleUnstaged; VisibleStaged = visibleStaged; Unstaged = unstaged; Staged = staged; - SelectedUnstaged = selectedUnstaged; - SelectedStaged = selectedStaged; + SelectedUnstaged = new(selectedUnstaged); + SelectedStaged = new(selectedStaged); _isLoadingData = false; UpdateInProgressState(); @@ -322,7 +374,7 @@ public async Task StageChangesAsync(List changes, Models.Change n return; IsStaging = true; - _selectedUnstaged = next != null ? [next] : []; + _selectedUnstaged = new(next != null ? new List { next } : null); using var lockWatcher = _repo.LockWatcher(); @@ -349,7 +401,7 @@ public async Task UnstageChangesAsync(List changes, Models.Change return; IsUnstaging = true; - _selectedStaged = next != null ? [next] : []; + _selectedStaged = new(next != null ? new List { next } : null); using var lockWatcher = _repo.LockWatcher(); @@ -388,10 +440,16 @@ public async Task SaveChangesToPatchAsync(List changes, bool isUn _repo.SendNotification(App.Text("SaveAsPatchSuccess")); } - public void Discard(List changes) + public void DiscardAllChanges() { if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Discard(_repo, changes)); + _repo.ShowPopup(new Discard(_repo)); + } + + public void Discard(List changes, Models.Change next) + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new Discard(_repo, changes, next)); } public void ClearFilter() @@ -574,7 +632,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) @@ -621,13 +679,13 @@ public async Task CommitAsync(bool autoStage, bool autoPush) if (rs == Models.ConfirmEmptyCommitResult.StageAllAndCommit) autoStage = true; else if (rs == Models.ConfirmEmptyCommitResult.StageSelectedAndCommit) - await StageChangesAsync(_selectedUnstaged, null); + await StageChangesAsync(_selectedUnstaged.Changes, null); } } using var lockWatcher = _repo.LockWatcher(); IsCommitting = true; - _repo.Settings.PushCommitMessage(_commitMessage); + _repo.UIStates.AddRecentCommitMessage(_commitMessage); if (autoStage && _unstaged.Count > 0) await StageChangesAsync(_unstaged, null); @@ -659,6 +717,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; } @@ -688,15 +747,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); @@ -708,7 +768,11 @@ public async Task CommitAsync(bool autoStage, bool autoPush) private List GetStagedChanges(List cached) { if (_useAmend) - return new Commands.QueryStagedChangesWithAmend(_repo.FullPath).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) @@ -721,10 +785,10 @@ public async Task CommitAsync(bool autoStage, bool autoPush) private void UpdateDetail() { - if (_selectedUnstaged.Count == 1) - SetDetail(_selectedUnstaged[0], true); - else if (_selectedStaged.Count == 1) - SetDetail(_selectedStaged[0], false); + if (_selectedUnstaged is { Count: 1, HasFolder: false }) + SetDetail(_selectedUnstaged.Changes[0], true); + else if (_selectedStaged is { Count: 1, HasFolder: false }) + SetDetail(_selectedStaged.Changes[0], false); else SetDetail(null, false); } @@ -747,6 +811,12 @@ private void UpdateInProgressState() if (_inProgressContext != null && _inProgressContext.GetType() == oldType && !string.IsNullOrEmpty(_commitMessage)) return; + // The operation that filled the commit message ended outside SourceGit (for example, a + // rebase or merge finished/aborted from the terminal). Drop the stale message unless the + // user has edited it since it was loaded. + if (_inProgressContext == null && oldType != null && IsAutoLoadedCommitMessage()) + CommitMessage = string.Empty; + if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "MERGE_MSG"))) return; @@ -756,7 +826,7 @@ private void UpdateInProgressState() if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "rebase-merge", "message"))) return; - CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResult(); + SetAutoLoadedCommitMessage(new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResult()); } private bool LoadCommitMessageFromFile(string file) @@ -768,10 +838,22 @@ private bool LoadCommitMessageFromFile(string file) if (string.IsNullOrEmpty(msg)) return false; - CommitMessage = msg; + SetAutoLoadedCommitMessage(msg); return true; } + private void SetAutoLoadedCommitMessage(string msg) + { + _autoLoadedCommitMessage = msg; + CommitMessage = msg; + } + + private bool IsAutoLoadedCommitMessage() + { + return !string.IsNullOrEmpty(_autoLoadedCommitMessage) && + string.Equals(_autoLoadedCommitMessage, _commitMessage, StringComparison.Ordinal); + } + private void SetDetail(Models.Change change, bool isUnstaged) { if (_isLoadingData) @@ -801,6 +883,66 @@ private bool IsChanged(List old, List cur) return false; } + private Models.Change FindFirstConflict(List conflicts) + { + if (conflicts.Count == 1 || Preferences.Instance.ChangeViewMode != Models.ChangeViewMode.Tree) + return conflicts[0]; + + var finder = (ChangeInTree l, ChangeInTree r) => + { + var lDirs = l.PathSegments.Length - 1; + var rDirs = r.PathSegments.Length - 1; + if (lDirs == 0) + { + if (rDirs == 0) + return Models.NumericSort.Compare(l.PathSegments[0], r.PathSegments[0]) <= 0 ? l : r; + else + return r; + } + else if (rDirs == 0) + { + return l; + } + + var min = Math.Min(lDirs, rDirs); + for (var idx = 0; idx < min; idx++) + { + var cmp = Models.NumericSort.Compare(l.PathSegments[idx], r.PathSegments[idx]); + if (cmp < 0) + return l; + else if (cmp > 0) + return r; + } + + if (lDirs == rDirs) + return Models.NumericSort.Compare(l.PathSegments[^1], r.PathSegments[^1]) <= 0 ? l : r; + + return lDirs < rDirs ? r : l; + }; + + var collection = new List(); + foreach (var c in conflicts) + collection.Add(new ChangeInTree(c)); + + var first = collection[0]; + for (var idx = 1; idx < collection.Count; idx++) + first = finder(first, collection[idx]); + + return first.Change; + } + + private class ChangeInTree + { + public Models.Change Change { get; set; } + public string[] PathSegments { get; set; } + + public ChangeInTree(Models.Change change) + { + Change = change; + PathSegments = change.Path.Split('/', StringSplitOptions.RemoveEmptyEntries); + } + } + private Repository _repo = null; private bool _isLoadingData = false; private bool _isStaging = false; @@ -814,13 +956,14 @@ private bool IsChanged(List old, List cur) private List _visibleUnstaged = []; private List _staged = []; private List _visibleStaged = []; - private List _selectedUnstaged = []; - private List _selectedStaged = []; + private ChangeSelection _selectedUnstaged = new(null); + private ChangeSelection _selectedStaged = new(null); private object _detailContext = null; private string _filter = string.Empty; private string _commitMessage = string.Empty; private bool _hasUnsolvedConflicts = false; private InProgressContext _inProgressContext = null; + private string _autoLoadedCommitMessage = null; } } diff --git a/src/Views/AIAssistant.axaml b/src/Views/AIAssistant.axaml index 0be7672de3..9c1db462b1 100644 --- a/src/Views/AIAssistant.axaml +++ b/src/Views/AIAssistant.axaml @@ -49,7 +49,7 @@ - + - + - - + + + + + - - + + + + + - - diff --git a/src/Views/CaptionButtons.axaml.cs b/src/Views/CaptionButtons.axaml.cs index 806d9cb5ee..a225916733 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 8d8f56bc32..be9143d1bb 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -50,7 +50,7 @@ IsChecked="{Binding IsExpanded, Mode=OneWay}" IsVisible="{Binding IsFolder}"/> - + - + @@ -101,7 +101,7 @@ Foreground="DarkOrange" FontWeight="Bold" Margin="4,0,0,0" IsVisible="{Binding IsConflicted}"/> - + @@ -137,7 +137,7 @@ FontWeight="Bold" Margin="4,0,0,0" IsVisible="{Binding IsConflicted}"/> - + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index bad63709e1..81b7030d61 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); } @@ -35,66 +39,119 @@ public class ChangeCollectionContainer : ListBoxEx 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) + { + 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) { - this.FindAncestorOfType()?.ToggleNodeIsExpanded(node); - e.Handled = true; + 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) 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 SelectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Selection), + static o => o.Selection, + static (o, v) => o.Selection = v); - public List SelectedChanges + public ViewModels.ChangeSelection Selection { - get => GetValue(SelectedChangesProperty); - set => SetValue(SelectedChangesProperty, value); + get => _selection; + set => SetAndRaise(SelectionProperty, ref _selection, 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 = _selection.Changes; var changes = Changes; if (selected == null || selected.Count == 0) return changes.Count > 0 ? changes[0] : null; @@ -222,7 +279,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang UpdateDataSource(true); else if (change.Property == ChangesProperty) UpdateDataSource(false); - else if (change.Property == SelectedChangesProperty) + else if (change.Property == SelectionProperty) UpdateSelection(); if (change.Property == EnableCompactFoldersProperty && ViewMode == Models.ChangeViewMode.Tree) @@ -279,43 +336,14 @@ private void OnRowDoubleTapped(object sender, TappedEventArgs e) private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) { - if (_disableSelectionChangingEvent) + if (_disableSelectionChangingEvent || sender is not ListBox listBox) return; _disableSelectionChangingEvent = true; - var selected = new List(); - if (sender is ListBox { SelectedItems: { } selectedItems }) - { - foreach (var item in selectedItems) - { - if (item is Models.Change c) - selected.Add(c); - else if (item is ViewModels.ChangeTreeNode node) - CollectChangesInNode(selected, node); - } - } - - var old = SelectedChanges ?? []; - if (old.Count != selected.Count) - { - SetCurrentValue(SelectedChangesProperty, selected); - } - else - { - bool allEquals = true; - foreach (var c in old) - { - if (!selected.Contains(c)) - { - allEquals = false; - break; - } - } - - if (!allEquals) - SetCurrentValue(SelectedChangesProperty, selected); - } + var selection = new ViewModels.ChangeSelection(listBox.SelectedItems); + if (selection.IsChanged(_selection)) + Selection = selection; _disableSelectionChangingEvent = false; } @@ -337,7 +365,7 @@ private void UpdateDataSource(bool onlyViewModeChange) { _disableSelectionChangingEvent = !onlyViewModeChange; - var changes = Changes; + var changes = _changes; if (changes == null || changes.Count == 0) { Content = null; @@ -345,10 +373,10 @@ private void UpdateDataSource(bool onlyViewModeChange) return; } - var selected = SelectedChanges ?? []; + var selected = _selection?.Changes ?? []; 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) @@ -409,7 +437,7 @@ private void UpdateSelection() _disableSelectionChangingEvent = true; - var selected = SelectedChanges ?? []; + var selected = _selection?.Changes ?? []; if (Content is ViewModels.ChangeCollectionAsTree tree) { tree.SelectedRows.Clear(); @@ -417,7 +445,6 @@ private void UpdateSelection() if (selected.Count > 0) { var sets = new HashSet(selected); - var nodes = new List(); foreach (var row in tree.Rows) { @@ -444,19 +471,6 @@ private void UpdateSelection() _disableSelectionChangingEvent = false; } - private void CollectChangesInNode(List outs, ViewModels.ChangeTreeNode node) - { - if (node.IsFolder) - { - foreach (var child in node.Children) - CollectChangesInNode(outs, child); - } - else if (!outs.Contains(node.Change)) - { - outs.Add(node.Change); - } - } - private void UpdateRowTips(Control control, Models.Change change) { var tip = new TextBlock() { TextWrapping = TextWrapping.Wrap }; @@ -472,6 +486,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 ViewModels.ChangeSelection _selection = new(null); private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index cbb47842da..ee48cbc895 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(); @@ -87,5 +92,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang 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 a1d6cef026..bf3d7b3db2 100644 --- a/src/Views/ChangeSubmoduleUrl.axaml +++ b/src/Views/ChangeSubmoduleUrl.axaml @@ -17,7 +17,7 @@ Classes="bold" Text="{DynamicResource Text.ChangeSubmoduleUrl}"/> - + 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 0000000000..583a869991 --- /dev/null +++ b/src/Views/Chart.cs @@ -0,0 +1,297 @@ +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) + 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 tickPen = new Pen(Brushes.Gray); + 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)); + } + + if (count == 0) + return; + + 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; + var labelsToDraw = new List(); + + using (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; + labelsToDraw.Add(new(formattedLabel, labelStartX, labelCenterX)); + maxLabelEndX = labelStartX - 16.0; + } + } + + if (x <= leftMargin) + break; + + x -= step; + + time = samples.NextSampleTime(time); + if (time < minTime) + break; + } while (true); + } + + foreach (var label in labelsToDraw) + { + context.DrawLine(tickPen, new Point(label.TickPosition, maxSampleH + 4), new Point(label.TickPosition, maxSampleH)); + context.DrawText(label.Text, new Point(label.TextPosition, maxSampleH + 6)); + } + } + + 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 record XLabel(FormattedText Text, double TextPosition, double TickPosition); + + 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/CheckoutAsWorktree.axaml b/src/Views/CheckoutAsWorktree.axaml new file mode 100644 index 0000000000..825f608acc --- /dev/null +++ b/src/Views/CheckoutAsWorktree.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutAsWorktree.axaml.cs b/src/Views/CheckoutAsWorktree.axaml.cs new file mode 100644 index 0000000000..7a4ad196ae --- /dev/null +++ b/src/Views/CheckoutAsWorktree.axaml.cs @@ -0,0 +1,41 @@ +using System; + +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views +{ + public partial class CheckoutAsWorktree : UserControl + { + public CheckoutAsWorktree() + { + InitializeComponent(); + } + + private async void SelectLocation(object _, RoutedEventArgs e) + { + var toplevel = TopLevel.GetTopLevel(this); + if (toplevel == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + try + { + var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) + { + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); + TxtLocation.Text = folderPath.TrimEnd('\\', '/'); + } + } + catch (Exception exception) + { + Models.Notification.Send(null, $"Failed to select location: {exception.Message}", true); + } + + e.Handled = true; + } + } +} diff --git a/src/Views/CheckoutBranchFromStash.axaml b/src/Views/CheckoutBranchFromStash.axaml index 0e03089686..037410af52 100644 --- a/src/Views/CheckoutBranchFromStash.axaml +++ b/src/Views/CheckoutBranchFromStash.axaml @@ -17,7 +17,7 @@ Classes="bold" Text="{DynamicResource Text.CheckoutBranchFromStash}"/> - + + x:Class="SourceGit.Views.CheckoutDetached" + x:DataType="vm:CheckoutDetached"> + Text="{DynamicResource Text.CheckoutDetached}"/> @@ -24,16 +25,36 @@ - + - - - - - + Text="{DynamicResource Text.CheckoutDetached.Target}" /> + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutCommit.axaml.cs b/src/Views/CheckoutDetached.axaml.cs similarity index 57% rename from src/Views/CheckoutCommit.axaml.cs rename to src/Views/CheckoutDetached.axaml.cs index 375816c938..8f2221cb52 100644 --- a/src/Views/CheckoutCommit.axaml.cs +++ b/src/Views/CheckoutDetached.axaml.cs @@ -2,9 +2,9 @@ namespace SourceGit.Views { - public partial class CheckoutCommit : UserControl + public partial class CheckoutDetached : UserControl { - public CheckoutCommit() + public CheckoutDetached() { InitializeComponent(); } diff --git a/src/Views/CheckoutRemoteBranchAsWorktree.axaml b/src/Views/CheckoutRemoteBranchAsWorktree.axaml new file mode 100644 index 0000000000..84b68ae3ae --- /dev/null +++ b/src/Views/CheckoutRemoteBranchAsWorktree.axaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs b/src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs new file mode 100644 index 0000000000..cff9995bbe --- /dev/null +++ b/src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs @@ -0,0 +1,41 @@ +using System; + +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views +{ + public partial class CheckoutRemoteBranchAsWorktree : UserControl + { + public CheckoutRemoteBranchAsWorktree() + { + InitializeComponent(); + } + + private async void SelectLocation(object _, RoutedEventArgs e) + { + var toplevel = TopLevel.GetTopLevel(this); + if (toplevel == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + try + { + var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) + { + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); + TxtLocation.Text = folderPath.TrimEnd('\\', '/'); + } + } + catch (Exception exception) + { + Models.Notification.Send(null, $"Failed to select location: {exception.Message}", true); + } + + e.Handled = true; + } + } +} diff --git a/src/Views/CherryPick.axaml b/src/Views/CherryPick.axaml index 1d8d15b630..85cd19514c 100644 --- a/src/Views/CherryPick.axaml +++ b/src/Views/CherryPick.axaml @@ -18,7 +18,7 @@ Classes="bold" Text="{DynamicResource Text.CherryPick}"/> - + - - + + @@ -73,8 +81,16 @@ - - + + diff --git a/src/Views/ChromelessWindow.cs b/src/Views/ChromelessWindow.cs index b05785b1fd..0fc83764ba 100644 --- a/src/Views/ChromelessWindow.cs +++ b/src/Views/ChromelessWindow.cs @@ -3,20 +3,12 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; namespace SourceGit.Views { public class ChromelessWindow : Window { - public static readonly StyledProperty LeftCaptionButtonWidthProperty = - AvaloniaProperty.Register(nameof(LeftCaptionButtonWidth), 72.0); - - public double LeftCaptionButtonWidth - { - get => GetValue(LeftCaptionButtonWidthProperty); - set => SetValue(LeftCaptionButtonWidthProperty, value); - } - public bool UseSystemWindowFrame { get => Native.OS.UseSystemWindowFrame; @@ -32,9 +24,10 @@ public bool CloseOnESC public ChromelessWindow() { - LeftCaptionButtonWidth = 72.0 / Math.Max(1.0, ViewModels.Preferences.Instance.Zoom); Focusable = true; Native.OS.SetupForWindow(this); + PositionChanged += (_, _) => UpdateEdgeSnapInsets(); + Resized += (_, _) => UpdateEdgeSnapInsets(); } public void BeginMoveWindow(object _, PointerPressedEventArgs e) @@ -84,23 +77,23 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) } } + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (OperatingSystem.IsWindows()) + Native.Win64Utilities.FixWindowFrame(this); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - if (change.Property == WindowStateProperty && OperatingSystem.IsWindows()) - { - if (WindowState == WindowState.Maximized) - { - BorderThickness = new Thickness(0); - Padding = new Thickness(8, 6, 8, 8); - } - else - { - BorderThickness = new Thickness(1); - Padding = new Thickness(0); - } - } + if (OperatingSystem.IsWindows() && change.Property == WindowStateProperty) + Native.Win64Utilities.FixWindowFrame(this); + + if (change.Property == WindowStateProperty) + UpdateEdgeSnapInsets(); } protected override void OnKeyDown(KeyEventArgs e) @@ -123,14 +116,17 @@ protected override void OnKeyDown(KeyEventArgs e) { var zoom = Math.Min(ViewModels.Preferences.Instance.Zoom + 0.05, 2.5); ViewModels.Preferences.Instance.Zoom = zoom; - LeftCaptionButtonWidth = 72.0 / zoom; e.Handled = true; } else if (e.Key == Key.OemMinus) { var zoom = Math.Max(ViewModels.Preferences.Instance.Zoom - 0.05, 1); ViewModels.Preferences.Instance.Zoom = zoom; - LeftCaptionButtonWidth = 72.0 / zoom; + e.Handled = true; + } + else if (e.Key == Key.W) + { + Close(); e.Handled = true; } } @@ -141,5 +137,47 @@ private void OnWindowBorderPointerPressed(object sender, PointerPressedEventArgs if (sender is Border { Tag: WindowEdge edge } && CanResize) BeginResizeDrag(edge, e); } + + // The transparent padding around the window holds the self-drawn shadow + // (keep the value in sync with "Window.custom_window_frame" style). + // Collapse it on the side touching a screen edge so the content sits flush. + private void UpdateEdgeSnapInsets() + { + if (!Classes.Contains("custom_window_frame")) + return; + + if (WindowState != WindowState.Normal) + { + // Let the "WindowState=Maximized" style take over + ClearValue(PaddingProperty); + return; + } + + var screen = Screens.ScreenFromWindow(this); + if (screen == null) + return; + + var workArea = screen.WorkingArea; + var right = Position.X + Bounds.Width; + var bottom = Position.Y + Bounds.Height; + + const double tolerance = 2.0; + var snapTop = Position.Y <= workArea.Y + tolerance; + var snapLeft = Position.X <= workArea.X + tolerance; + var snapRight = right >= workArea.Right - tolerance; + var snapBottom = bottom >= workArea.Bottom - tolerance; + + if (!snapTop && !snapLeft && !snapRight && !snapBottom) + { + ClearValue(PaddingProperty); + return; + } + + Padding = new Thickness( + snapLeft ? 0 : 12, + snapTop ? 0 : 12, + snapRight ? 0 : 12, + snapBottom ? 0 : 12); + } } } diff --git a/src/Views/Cleanup.axaml b/src/Views/Cleanup.axaml index 78c42ac015..18dbb9eea4 100644 --- a/src/Views/Cleanup.axaml +++ b/src/Views/Cleanup.axaml @@ -16,7 +16,7 @@ Classes="bold" Text="{DynamicResource Text.Repository.Clean}"/> - + diff --git a/src/Views/Clone.axaml b/src/Views/Clone.axaml index a501474bec..b6cd696a8e 100644 --- a/src/Views/Clone.axaml +++ b/src/Views/Clone.axaml @@ -35,7 +35,7 @@ Data="{StaticResource Icons.Link}" Fill="{DynamicResource Brush.FG1}"/> - + @@ -59,7 +59,7 @@ Data="{StaticResource Icons.Password}" Fill="{DynamicResource Brush.FG1}"/> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitSearchView.axaml.cs b/src/Views/CommitSearchView.axaml.cs new file mode 100644 index 0000000000..0df4ca04a5 --- /dev/null +++ b/src/Views/CommitSearchView.axaml.cs @@ -0,0 +1,102 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; + +namespace SourceGit.Views +{ + public partial class CommitSearchView : UserControl + { + public CommitSearchView() + { + InitializeComponent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsVisibleProperty && IsVisible) + Dispatcher.UIThread.Post(() => TxtSearchCommitsBox.Focus(NavigationMethod.Directional), DispatcherPriority.Background); + } + + private void OnSearchBoxKeyDown(object _, KeyEventArgs e) + { + if (DataContext is not ViewModels.SearchCommitContext ctx) + return; + + if (e.Key == Key.Enter) + { + ctx.StartSearch(); + e.Handled = true; + } + else if (e.Key == Key.Down) + { + if (ctx.Suggestions is { Count: > 0 }) + { + SearchSuggestionBox.Focus(NavigationMethod.Tab); + SearchSuggestionBox.SelectedIndex = 0; + } + + e.Handled = true; + } + else if (e.Key == Key.Escape) + { + ctx.ClearSuggestions(); + e.Handled = true; + } + } + + private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e) + { + if (DataContext is not ViewModels.SearchCommitContext ctx) + return; + + if (e.Key == Key.Escape) + { + ctx.ClearSuggestions(); + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + var selected = SearchSuggestionBox.SelectedItem; + if (selected is string content) + { + ctx.Filter = content; + TxtSearchCommitsBox.CaretIndex = content.Length; + } + else if (selected is Models.User user) + { + var apply = user.ToString().EscapeForBRE(); + ctx.Filter = apply; + TxtSearchCommitsBox.CaretIndex = apply.Length; + } + + ctx.StartSearch(); + e.Handled = true; + } + } + + private void OnSearchSuggestionTapped(object sender, TappedEventArgs e) + { + if (DataContext is not ViewModels.SearchCommitContext ctx) + return; + + var selected = (sender as Control)?.DataContext; + if (selected is string content) + { + ctx.Filter = content; + TxtSearchCommitsBox.CaretIndex = content.Length; + } + else if (selected is Models.User user) + { + var apply = user.ToString().EscapeForBRE(); + ctx.Filter = apply; + TxtSearchCommitsBox.CaretIndex = apply.Length; + } + + ctx.StartSearch(); + e.Handled = true; + } + } +} diff --git a/src/Views/CommitStatusIndicator.cs b/src/Views/CommitStatusIndicator.cs index 49d74c98fe..08d29572be 100644 --- a/src/Views/CommitStatusIndicator.cs +++ b/src/Views/CommitStatusIndicator.cs @@ -8,13 +8,16 @@ namespace SourceGit.Views { public class CommitStatusIndicator : Control { - public static readonly StyledProperty CurrentBranchProperty = - AvaloniaProperty.Register(nameof(CurrentBranch)); + public static readonly DirectProperty CurrentBranchProperty = + AvaloniaProperty.RegisterDirect( + nameof(CurrentBranch), + static o => o.CurrentBranch, + static (o, v) => o.CurrentBranch = v); public Models.Branch CurrentBranch { - get => GetValue(CurrentBranchProperty); - set => SetValue(CurrentBranchProperty, value); + get => _currentBranch; + set => SetAndRaise(CurrentBranchProperty, ref _currentBranch, value); } public static readonly StyledProperty AheadBrushProperty = @@ -52,13 +55,13 @@ public override void Render(DrawingContext context) protected override Size MeasureOverride(Size availableSize) { - if (DataContext is Models.Commit commit && CurrentBranch is { } b) + if (DataContext is Models.Commit commit && _currentBranch != null) { var sha = commit.SHA; - if (b.Ahead.Contains(sha)) + if (_currentBranch.Ahead.Contains(sha)) _status = Status.Ahead; - else if (b.Behind.Contains(sha)) + else if (_currentBranch.Behind.Contains(sha)) _status = Status.Behind; else _status = Status.Normal; @@ -84,6 +87,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang InvalidateMeasure(); } + private Models.Branch _currentBranch = null; private Status _status = Status.Normal; } } diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index fb1ecac4f3..6949072746 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -77,8 +77,17 @@ public IBrush LinkForeground set => SetValue(LinkForegroundProperty, value); } + public static readonly StyledProperty InlineCodeForegroundProperty = + AvaloniaProperty.Register(nameof(InlineCodeForeground), Brushes.White); + + public IBrush InlineCodeForeground + { + get => GetValue(InlineCodeForegroundProperty); + set => SetValue(InlineCodeForegroundProperty, value); + } + public static readonly StyledProperty ShowStrikethroughProperty = - AvaloniaProperty.Register(nameof(ShowStrikethrough), false); + AvaloniaProperty.Register(nameof(ShowStrikethrough)); public bool ShowStrikethrough { @@ -86,22 +95,28 @@ public bool ShowStrikethrough set => SetValue(ShowStrikethroughProperty, value); } - public static readonly StyledProperty SubjectProperty = - AvaloniaProperty.Register(nameof(Subject)); + public static readonly DirectProperty SubjectProperty = + AvaloniaProperty.RegisterDirect( + nameof(Subject), + static o => o.Subject, + static (o, v) => o.Subject = v); public string Subject { - get => GetValue(SubjectProperty); - set => SetValue(SubjectProperty, value); + get => _subject; + set => SetAndRaise(SubjectProperty, ref _subject, value); } - public static readonly StyledProperty> IssueTrackersProperty = - AvaloniaProperty.Register>(nameof(IssueTrackers)); + public static readonly DirectProperty> IssueTrackersProperty = + AvaloniaProperty.RegisterDirect>( + nameof(IssueTrackers), + static o => o.IssueTrackers, + static (o, v) => o.IssueTrackers = v); public AvaloniaList IssueTrackers { - get => GetValue(IssueTrackersProperty); - set => SetValue(IssueTrackersProperty, value); + get => _issueTrackers; + set => SetAndRaise(IssueTrackersProperty, ref _issueTrackers, value); } public override void Render(DrawingContext context) @@ -248,7 +263,15 @@ private void GenerateInlineElements() foreach (var rule in rules) rule.Matches(_elements, subject); - if (subject.StartsWith('[')) + if (subject.StartsWith("fixup! ", StringComparison.Ordinal) || subject.StartsWith("amend! ", StringComparison.Ordinal)) + { + _elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, 6, string.Empty)); + } + else if (subject.StartsWith("squash! ", StringComparison.Ordinal)) + { + _elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, 7, string.Empty)); + } + else if (subject.StartsWith('[')) { var bracketIdx = subject.IndexOf(']'); if (bracketIdx > 1 && bracketIdx < 50 && _elements.Intersect(0, bracketIdx + 1) == null) @@ -297,6 +320,7 @@ private void GenerateFormattedTextElements() var codeFontFamily = CodeFontFamily; var fontSize = FontSize; var foreground = Foreground; + var inlineCodeForeground = InlineCodeForeground; var linkForeground = LinkForeground; var typeface = new Typeface(fontFamily, FontStyle.Normal, FontWeight); var codeTypeface = new Typeface(codeFontFamily, FontStyle.Normal, FontWeight); @@ -351,7 +375,7 @@ private void GenerateFormattedTextElements() FlowDirection.LeftToRight, codeTypeface, fontSize - 0.5, - foreground); + inlineCodeForeground); _inlines.Add(new Inline(x, link, elem)); x += link.WidthIncludingTrailingWhitespace + 8; } @@ -400,6 +424,8 @@ public Inline(double x, FormattedText text, Models.InlineElement elem) } } + private string _subject = string.Empty; + private AvaloniaList _issueTrackers = null; private Models.InlineElementCollector _elements = new(); private Models.InlineElement _lastHover = null; private List _inlines = []; diff --git a/src/Views/CommitTimeTextBlock.cs b/src/Views/CommitTimeTextBlock.cs index dcd0dd283a..66c4090ab3 100644 --- a/src/Views/CommitTimeTextBlock.cs +++ b/src/Views/CommitTimeTextBlock.cs @@ -10,40 +10,52 @@ namespace SourceGit.Views { public class CommitTimeTextBlock : TextBlock { - public static readonly StyledProperty ShowAsDateTimeProperty = - AvaloniaProperty.Register(nameof(ShowAsDateTime), true); + public static readonly DirectProperty ShowAsDateTimeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ShowAsDateTime), + static o => o.ShowAsDateTime, + static (o, v) => o.ShowAsDateTime = v); public bool ShowAsDateTime { - get => GetValue(ShowAsDateTimeProperty); - set => SetValue(ShowAsDateTimeProperty, value); + get => _showAsDateTime; + set => SetAndRaise(ShowAsDateTimeProperty, ref _showAsDateTime, value); } - public static readonly StyledProperty Use24HoursProperty = - AvaloniaProperty.Register(nameof(Use24Hours), true); + public static readonly DirectProperty Use24HoursProperty = + AvaloniaProperty.RegisterDirect( + nameof(Use24Hours), + static o => o.Use24Hours, + static (o, v) => o.Use24Hours = v); public bool Use24Hours { - get => GetValue(Use24HoursProperty); - set => SetValue(Use24HoursProperty, value); + get => _use24Hours; + set => SetAndRaise(Use24HoursProperty, ref _use24Hours, value); } - public static readonly StyledProperty DateTimeFormatProperty = - AvaloniaProperty.Register(nameof(DateTimeFormat)); + public static readonly DirectProperty DateTimeFormatProperty = + AvaloniaProperty.RegisterDirect( + nameof(DateTimeFormat), + static o => o.DateTimeFormat, + static (o, v) => o.DateTimeFormat = v); public int DateTimeFormat { - get => GetValue(DateTimeFormatProperty); - set => SetValue(DateTimeFormatProperty, value); + get => _dateTimeFormat; + set => SetAndRaise(DateTimeFormatProperty, ref _dateTimeFormat, value); } - public static readonly StyledProperty UseAuthorTimeProperty = - AvaloniaProperty.Register(nameof(UseAuthorTime), true); + public static readonly DirectProperty TimestampProperty = + AvaloniaProperty.RegisterDirect( + nameof(Timestamp), + static o => o.Timestamp, + static (o, v) => o.Timestamp = v); - public bool UseAuthorTime + public ulong Timestamp { - get => GetValue(UseAuthorTimeProperty); - set => SetValue(UseAuthorTimeProperty, value); + get => _timestamp; + set => SetAndRaise(TimestampProperty, ref _timestamp, value); } protected override Type StyleKeyOverride => typeof(TextBlock); @@ -52,7 +64,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { base.OnPropertyChanged(change); - if (change.Property == UseAuthorTimeProperty) + if (change.Property == TimestampProperty) { SetCurrentValue(TextProperty, GetDisplayText()); } @@ -62,12 +74,12 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (ShowAsDateTime) { - StopTimer(); + _refreshTimer?.Stop(); HorizontalAlignment = HorizontalAlignment.Left; } else { - StartTimer(); + _refreshTimer?.Start(); HorizontalAlignment = HorizontalAlignment.Center; } } @@ -82,14 +94,27 @@ protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - if (!ShowAsDateTime) - StartTimer(); + _refreshTimer = new DispatcherTimer(); + _refreshTimer.Interval = TimeSpan.FromSeconds(10); + _refreshTimer.Tag = this; + _refreshTimer.Tick += static (o, _) => + { + if (o is DispatcherTimer { Tag: CommitTimeTextBlock textBlock }) + { + var text = textBlock.GetDisplayText(); + if (!text.Equals(textBlock.Text, StringComparison.Ordinal)) + textBlock.Text = text; + } + }; + _refreshTimer.IsEnabled = !ShowAsDateTime; } protected override void OnUnloaded(RoutedEventArgs e) { + _refreshTimer.Tag = null; + _refreshTimer.IsEnabled = false; + base.OnUnloaded(e); - StopTimer(); } protected override void OnDataContextChanged(EventArgs e) @@ -98,35 +123,9 @@ protected override void OnDataContextChanged(EventArgs e) SetCurrentValue(TextProperty, GetDisplayText()); } - private void StartTimer() - { - if (_refreshTimer != null) - return; - - _refreshTimer = DispatcherTimer.Run(() => - { - var text = GetDisplayText(); - if (!text.Equals(Text, StringComparison.Ordinal)) - Text = text; - return true; - }, TimeSpan.FromSeconds(10)); - } - - private void StopTimer() - { - if (_refreshTimer != null) - { - _refreshTimer.Dispose(); - _refreshTimer = null; - } - } - private string GetDisplayText() { - if (DataContext is not Models.Commit commit) - return string.Empty; - - var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime; + var timestamp = Timestamp; if (ShowAsDateTime) return Models.DateTimeFormat.Format(timestamp); @@ -172,6 +171,10 @@ private string GetDisplayText() return App.Text("Period.YearsAgo", diffYear); } - private IDisposable _refreshTimer = null; + private bool _showAsDateTime = true; + private bool _use24Hours = true; + private int _dateTimeFormat = 0; + private ulong _timestamp = 0; + private DispatcherTimer _refreshTimer = null; } } diff --git a/src/Views/Compare.axaml b/src/Views/Compare.axaml index 7018064667..72f4a0a799 100644 --- a/src/Views/Compare.axaml +++ b/src/Views/Compare.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" @@ -12,7 +13,7 @@ Icon="/App.ico" Title="{DynamicResource Text.Compare}" MinWidth="1280" MinHeight="720"> - + @@ -50,12 +51,14 @@ - + @@ -71,6 +74,13 @@ @@ -88,7 +98,14 @@ - + - - - + + + + + + + + + + + + + + + + + @@ -137,27 +172,29 @@ + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=ChangeViewMode, Mode=TwoWay}"/> - - + + + + + IsVisible="{Binding IsLoadingChanges}"/> - + @@ -174,7 +211,7 @@ Focusable="False"/> - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Compare.axaml.cs b/src/Views/Compare.axaml.cs index a65bf39e69..488220e5eb 100644 --- a/src/Views/Compare.axaml.cs +++ b/src/Views/Compare.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; @@ -17,11 +18,8 @@ public Compare() private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.Compare { SelectedChanges: { Count: > 0 } selected } vm && - sender is ChangeCollectionView view) + if (DataContext is ViewModels.Compare { ChangeSelection: { Count: > 0 } selection } vm) { - var menu = new ContextMenu(); - var patch = new MenuItem(); patch.Header = App.Text("FileCM.SaveAsPatch"); patch.Icon = this.CreateMenuIcon("Icons.Save"); @@ -42,7 +40,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e if (storageFile != null) { var saveTo = storageFile.Path.LocalPath; - var succ = await vm.SaveChangesAsPatchAsync(selected, saveTo); + var succ = await vm.SaveChangesAsPatchAsync(selection.Changes, saveTo); if (succ) await new Alert().ShowAsync(this, "Save patch successfully.", false); } @@ -55,36 +53,47 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e e.Handled = true; }; - if (selected.Count == 1) + var selectedSingleFolder = selection.IsSingleFolder; + var fullPathOfFolder = selectedSingleFolder ? vm.GetAbsPath(selection.SingleFolderPath) : null; + var relativePathOfFolder = selectedSingleFolder ? selection.SingleFolderPath : null; + + var menu = new ContextMenu(); + if (selection.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) => + var change = selection.Changes[0]; + var changeFullPath = vm.GetAbsPath(change.Path); + + if (!selection.HasFolder) { - vm.OpenInExternalDiffTool(change); - ev.Handled = true; - }; - menu.Items.Add(openWithMerger); + 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) + if (!selection.HasFolder || selectedSingleFolder) { - 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.IsEnabled = selectedSingleFolder ? Directory.Exists(fullPathOfFolder) : File.Exists(changeFullPath); explore.Click += (_, ev) => { - Native.OS.OpenInFileManager(full); + Native.OS.OpenInFileManager(selectedSingleFolder ? fullPathOfFolder : changeFullPath); ev.Handled = true; }; menu.Items.Add(explore); } - menu.Items.Add(new MenuItem() { Header = "-" }); + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(patch); if (vm.CanResetFiles) @@ -118,7 +127,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - await this.CopyTextAsync(change.Path); + await this.CopyTextAsync(selectedSingleFolder ? relativePathOfFolder : change.Path); ev.Handled = true; }; @@ -128,7 +137,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, ev) => { - await this.CopyTextAsync(vm.GetAbsPath(change.Path)); + await this.CopyTextAsync(selectedSingleFolder ? fullPathOfFolder : changeFullPath); ev.Handled = true; }; @@ -138,6 +147,22 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e } else { + if (selectedSingleFolder) + { + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = Directory.Exists(fullPathOfFolder); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(fullPathOfFolder); + ev.Handled = true; + }; + + menu.Items.Add(explore); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + menu.Items.Add(patch); if (vm.CanResetFiles) @@ -147,7 +172,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e resetToLeft.Icon = this.CreateMenuIcon("Icons.File.Checkout"); resetToLeft.Click += async (_, ev) => { - await vm.ResetMultipleToLeftAsync(selected); + await vm.ResetMultipleToLeftAsync(selection.Changes); ev.Handled = true; }; @@ -156,7 +181,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e resetToRight.Icon = this.CreateMenuIcon("Icons.File.Checkout"); resetToRight.Click += async (_, ev) => { - await vm.ResetMultipleToRightAsync(selected); + await vm.ResetMultipleToRightAsync(selection.Changes); ev.Handled = true; }; @@ -171,11 +196,19 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - var builder = new StringBuilder(); - foreach (var c in selected) - builder.AppendLine(c.Path); + if (selectedSingleFolder) + { + await this.CopyTextAsync(relativePathOfFolder); + } + else + { + var builder = new StringBuilder(); + foreach (var c in selection.Changes) + builder.AppendLine(c.Path); + + await this.CopyTextAsync(builder.ToString()); + } - await this.CopyTextAsync(builder.ToString()); ev.Handled = true; }; @@ -185,11 +218,19 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e 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)); + if (selectedSingleFolder) + { + await this.CopyTextAsync(fullPathOfFolder); + } + else + { + var builder = new StringBuilder(); + foreach (var c in selection.Changes) + builder.AppendLine(vm.GetAbsPath(c.Path)); + + await this.CopyTextAsync(builder.ToString()); + } - await this.CopyTextAsync(builder.ToString()); ev.Handled = true; }; @@ -198,12 +239,78 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e menu.Items.Add(copyFullPath); } - menu.Open(view); + menu.Open(sender as Control); } 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) @@ -214,10 +321,7 @@ private void OnPressedSHA(object sender, PointerPressedEventArgs e) 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 }) + if (DataContext is not ViewModels.Compare { ChangeSelection: { Count: > 0 } selection } vm) return; var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; @@ -225,13 +329,17 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) { var builder = new StringBuilder(); var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - if (selectedChanges.Count == 1) + if (selection.IsSingleFolder) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.SingleFolderPath) : selection.SingleFolderPath); + } + else if (selection.Changes.Count == 1) { - builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.Changes[0].Path) : selection.Changes[0].Path); } else { - foreach (var c in selectedChanges) + foreach (var c in selection.Changes) builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); } diff --git a/src/Views/CompareCommandPalette.axaml b/src/Views/CompareCommandPalette.axaml index edd031b16f..e8b32115cc 100644 --- a/src/Views/CompareCommandPalette.axaml +++ b/src/Views/CompareCommandPalette.axaml @@ -32,7 +32,7 @@ Margin="4,0,0,0" Padding="4,0"> - + diff --git a/src/Views/Confirm.axaml b/src/Views/Confirm.axaml index 52b9240ab3..a9055f71ad 100644 --- a/src/Views/Confirm.axaml +++ b/src/Views/Confirm.axaml @@ -56,6 +56,7 @@ Width="80" Height="30" Margin="4,0" + HotKey="Escape" Click="CloseWindow" HorizontalAlignment="Center" HorizontalContentAlignment="Center" diff --git a/src/Views/ConfirmEmptyCommit.axaml b/src/Views/ConfirmEmptyCommit.axaml index 7ed1d12c32..43b5ae2544 100644 --- a/src/Views/ConfirmEmptyCommit.axaml +++ b/src/Views/ConfirmEmptyCommit.axaml @@ -50,7 +50,7 @@ Content="{DynamicResource Text.ConfirmEmptyCommit.StageSelectedThenCommit}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/> - + - - @@ -142,7 +142,7 @@ - + @@ -150,9 +150,10 @@ + ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.All}" + PropertyChanged="OnToggleButtonPropertyChanged"> @@ -172,20 +173,13 @@ - + - - - - + ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}" + PropertyChanged="OnToggleButtonPropertyChanged"> - - + + + diff --git a/src/Views/EditRemote.axaml.cs b/src/Views/EditRemote.axaml.cs index 7d88704e53..79e930455e 100644 --- a/src/Views/EditRemote.axaml.cs +++ b/src/Views/EditRemote.axaml.cs @@ -1,3 +1,6 @@ +using System; +using System.IO; + using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; @@ -17,7 +20,19 @@ private async void SelectSSHKey(object _, RoutedEventArgs e) if (toplevel == null) return; - var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var startupDir = Path.Combine(home, ".ssh"); + if (!Directory.Exists(startupDir)) + startupDir = home; + + var suggestedStartLocation = await toplevel.StorageProvider.TryGetFolderFromPathAsync(startupDir); + var options = new FilePickerOpenOptions() + { + AllowMultiple = false, + FileTypeFilter = [new("SSHKey") { Patterns = ["*"] }], + SuggestedStartLocation = suggestedStartLocation + }; + 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 a5016eebcb..8b9d663406 100644 --- a/src/Views/EditRepositoryNode.axaml +++ b/src/Views/EditRepositoryNode.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.EditRepositoryNode" x:DataType="vm:EditRepositoryNode"> @@ -24,49 +25,42 @@ IsVisible="{Binding IsRepository}"/> - - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Views/ExecuteCustomAction.axaml b/src/Views/ExecuteCustomAction.axaml index 6f086c0118..6769f94814 100644 --- a/src/Views/ExecuteCustomAction.axaml +++ b/src/Views/ExecuteCustomAction.axaml @@ -45,8 +45,17 @@ - - + + @@ -141,7 +150,7 @@ IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> - + diff --git a/src/Views/ExecuteCustomAction.axaml.cs b/src/Views/ExecuteCustomAction.axaml.cs index e3aadb39bc..cf75c5a7a7 100644 --- a/src/Views/ExecuteCustomAction.axaml.cs +++ b/src/Views/ExecuteCustomAction.axaml.cs @@ -47,7 +47,7 @@ private async void SelectPath(object sender, RoutedEventArgs e) var options = new FilePickerOpenOptions() { AllowMultiple = false, - FileTypeFilter = [new FilePickerFileType("File") { Patterns = ["*.*"] }] + FileTypeFilter = [new("All Files") { Patterns = ["*"] }] }; var selected = await topLevel.StorageProvider.OpenFilePickerAsync(options); diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index 1a60f3eb7d..c9de0e6092 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -17,7 +17,7 @@ Classes="bold" Text="{DynamicResource Text.Fetch.Title}"/> - + - + diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index eb5f90cd08..aa4f07864a 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -13,7 +13,7 @@ Icon="/App.ico" Title="{DynamicResource Text.FileHistory}" MinWidth="1280" MinHeight="720"> - + @@ -43,11 +43,8 @@ - - - - + @@ -57,12 +54,14 @@ @@ -88,6 +87,7 @@ @@ -144,46 +143,34 @@ - - - - - - - - - - - - - - - + + + + + + + + + + - - + + + @@ -207,7 +194,14 @@ - + 0 } selected } listBox) + { + var copySHA = new MenuItem(); + copySHA.Header = App.Text("SHALinkCM.CopySHA"); + copySHA.Icon = this.CreateMenuIcon("Icons.Copy"); + copySHA.Click += async (_, ev) => + { + var shas = new List(); + foreach (var item in selected) + { + if (item is Models.FileVersion ver) + shas.Add(ver.SHA); + } + await this.CopyTextAsync(string.Join("\n", shas)); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(copySHA); + menu.Open(listBox); + e.Handled = true; + } + } + private void OnPressCommitSHA(object sender, PointerPressedEventArgs e) { if (sender is TextBlock { DataContext: Models.FileVersion ver } && diff --git a/src/Views/FileModeChange.cs b/src/Views/FileModeChange.cs new file mode 100644 index 0000000000..44d6eb5c40 --- /dev/null +++ b/src/Views/FileModeChange.cs @@ -0,0 +1,140 @@ +using System.Globalization; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public class FileModeChange : Control + { + public static readonly DirectProperty OldModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(OldMode), + static o => o.OldMode, + static (o, v) => o.OldMode = v); + + public int OldMode + { + get => _oldMode; + set => SetAndRaise(OldModeProperty, ref _oldMode, value); + } + + public static readonly DirectProperty NewModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(NewMode), + static o => o.NewMode, + static (o, v) => o.NewMode = v); + + public int NewMode + { + get => _newMode; + set => SetAndRaise(NewModeProperty, ref _newMode, value); + } + + public static readonly StyledProperty FontFamilyProperty = + TextBlock.FontFamilyProperty.AddOwner(); + + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + public static readonly StyledProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(); + + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public static readonly StyledProperty ForegroundProperty = + TextBlock.ForegroundProperty.AddOwner(); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly StyledProperty BackgroundProperty = + TextBlock.BackgroundProperty.AddOwner(); + + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + public override void Render(DrawingContext context) + { + if (_label == null) + return; + + var rect = new Rect(4, 0, _label.WidthIncludingTrailingWhitespace + 8, Bounds.Height); + context.FillRectangle(Background, rect, 4); + context.DrawText(_label, new Point(8, 9 - _label.Height * 0.5)); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == OldModeProperty || + change.Property == NewModeProperty || + change.Property == FontFamilyProperty || + change.Property == ForegroundProperty) + InvalidateMeasure(); + else if (change.Property == BackgroundProperty) + InvalidateVisual(); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_oldMode == 0 && _newMode == 0) + { + _label = null; + ToolTip.SetTip(this, null); + return new Size(0, 0); + } + + _label = new FormattedText( + $"{_oldMode} -> {_newMode}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily), + FontSize, + Foreground); + + if (_oldMode == 0) + ToolTip.SetTip(this, $"{App.Text("FileModeChange.New")}{TranslateMode(_newMode)}"); + else if (_newMode == 0) + ToolTip.SetTip(this, $"{App.Text("FileModeChange.Deleted")}{TranslateMode(_oldMode)}"); + else + ToolTip.SetTip(this, $"{App.Text("FileModeChange")}{TranslateMode(_oldMode)} -> {TranslateMode(_newMode)}"); + + return new Size(_label.WidthIncludingTrailingWhitespace + 12, 18); + } + + private string TranslateMode(int mode) + { + var key = mode switch + { + 100644 => "FileModeChange.Normal", + 100755 => "FileModeChange.Executable", + 040000 => "FileModeChange.Directory", + 120000 => "FileModeChange.Symlink", + 160000 => "FileModeChange.Submodule", + _ => "FileModeChange.Unknown" + }; + + return App.Text(key); + } + + private int _oldMode = 0; + private int _newMode = 0; + private FormattedText _label = null; + } +} diff --git a/src/Views/FilterModeSwitchButton.axaml.cs b/src/Views/FilterModeSwitchButton.axaml.cs index d5504d43ed..91c20d823a 100644 --- a/src/Views/FilterModeSwitchButton.axaml.cs +++ b/src/Views/FilterModeSwitchButton.axaml.cs @@ -7,31 +7,36 @@ namespace SourceGit.Views { public partial class FilterModeSwitchButton : UserControl { - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); + public static readonly DirectProperty ModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(Mode), + static o => o.Mode, + static (o, v) => o.Mode = v); public Models.FilterMode Mode { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); + get => _mode; + set => SetAndRaise(ModeProperty, ref _mode, value); } - public static readonly StyledProperty IsNoneVisibleProperty = - AvaloniaProperty.Register(nameof(IsNoneVisible)); + public static readonly DirectProperty IsContextMenuOpeningProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsContextMenuOpening), + static o => o.IsContextMenuOpening); - public bool IsNoneVisible + public bool IsContextMenuOpening { - get => GetValue(IsNoneVisibleProperty); - set => SetValue(IsNoneVisibleProperty, value); + get => _isContextMenuOpening; + set => SetAndRaise(IsContextMenuOpeningProperty, ref _isContextMenuOpening, value); } - public static readonly StyledProperty IsContextMenuOpeningProperty = - AvaloniaProperty.Register(nameof(IsContextMenuOpening)); + public static readonly StyledProperty IsHoverParentProperty = + AvaloniaProperty.Register(nameof(IsHoverParent)); - public bool IsContextMenuOpening + public bool IsHoverParent { - get => GetValue(IsContextMenuOpeningProperty); - set => SetValue(IsContextMenuOpeningProperty, value); + get => GetValue(IsHoverParentProperty); + set => SetValue(IsHoverParentProperty, value); } public FilterModeSwitchButton() @@ -45,10 +50,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang base.OnPropertyChanged(change); if (change.Property == ModeProperty || - change.Property == IsNoneVisibleProperty || + change.Property == IsHoverParentProperty || change.Property == IsContextMenuOpeningProperty) { - var visible = (Mode != Models.FilterMode.None || IsNoneVisible || IsContextMenuOpening); + var visible = (Mode != Models.FilterMode.None || IsHoverParent || IsContextMenuOpening); SetCurrentValue(IsVisibleProperty, visible); } } @@ -71,7 +76,10 @@ private void OnChangeFilterModeButtonClicked(object sender, RoutedEventArgs e) else if (DataContext is ViewModels.BranchTreeNode branchNode) FillContextMenuForBranch(menu, repo, branchNode, branchNode.FilterMode); + menu.Closed += (_, _) => IsContextMenuOpening = false; menu.Open(button); + + IsContextMenuOpening = true; e.Handled = true; } @@ -90,11 +98,6 @@ private void FillContextMenuForTag(ContextMenu menu, ViewModels.Repository repo, menu.Items.Add(unset); menu.Items.Add(new MenuItem() { Header = "-" }); } - else - { - IsContextMenuOpening = true; - menu.Closed += (_, _) => IsContextMenuOpening = false; - } var include = new MenuItem(); include.Icon = this.CreateMenuIcon("Icons.Filter"); @@ -135,11 +138,6 @@ private void FillContextMenuForBranch(ContextMenu menu, ViewModels.Repository re menu.Items.Add(unset); menu.Items.Add(new MenuItem() { Header = "-" }); } - else - { - IsContextMenuOpening = true; - menu.Closed += (_, _) => IsContextMenuOpening = false; - } var include = new MenuItem(); include.Icon = this.CreateMenuIcon("Icons.Filter"); @@ -164,5 +162,8 @@ private void FillContextMenuForBranch(ContextMenu menu, ViewModels.Repository re menu.Items.Add(include); menu.Items.Add(exclude); } + + private Models.FilterMode _mode = Models.FilterMode.None; + private bool _isContextMenuOpening = false; } } diff --git a/src/Views/GitFlowFinish.axaml b/src/Views/GitFlowFinish.axaml index d0d8d21268..879d88d854 100644 --- a/src/Views/GitFlowFinish.axaml +++ b/src/Views/GitFlowFinish.axaml @@ -11,7 +11,7 @@ + Data="{StaticResource Icons.GitFlow.Finish}"/> - - + + + + - + ToolTip.Tip="--keep"/> diff --git a/src/Views/GitFlowStart.axaml b/src/Views/GitFlowStart.axaml index b9e7fdc9bb..51827c6d66 100644 --- a/src/Views/GitFlowStart.axaml +++ b/src/Views/GitFlowStart.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.GitFlowStart" x:DataType="vm:GitFlowStart"> @@ -26,18 +27,36 @@ Text="{DynamicResource Text.GitFlow.StartHotfixTitle}" IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Hotfix}}"/> - - - + + + + + + + + + + - + Text="{DynamicResource Text.GitFlow.StartAt}"/> + diff --git a/src/Views/GotoRevisionSelector.axaml b/src/Views/GotoRevisionSelector.axaml index dd2ae97bc4..d29d400906 100644 --- a/src/Views/GotoRevisionSelector.axaml +++ b/src/Views/GotoRevisionSelector.axaml @@ -35,7 +35,7 @@ IsCloseButtonOnly="True" IsVisible="{OnPlatform True, macOS=False}"/> - + - + - + - + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 0ae77e75ed..a3d24529fd 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -14,7 +14,7 @@ - + @@ -24,20 +24,10 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + - + @@ -128,8 +156,10 @@ Background="Transparent" IsVisible="{Binding CommandPalette, Converter={x:Static ObjectConverters.IsNotNull}}" PointerPressed="OnCloseCommandPalette"> - - + + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 7461fa4b38..0ac14e6bb3 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -1,33 +1,24 @@ -using System; +using System; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.VisualTree; namespace SourceGit.Views { public partial class Launcher : ChromelessWindow { - public static readonly StyledProperty CaptionHeightProperty = - AvaloniaProperty.Register(nameof(CaptionHeight)); + public static readonly DirectProperty CaptionHeightProperty = + AvaloniaProperty.RegisterDirect( + nameof(CaptionHeight), + static o => o.CaptionHeight); public GridLength CaptionHeight { - get => GetValue(CaptionHeightProperty); - set => SetValue(CaptionHeightProperty, value); - } - - public static readonly StyledProperty HasLeftCaptionButtonProperty = - AvaloniaProperty.Register(nameof(HasLeftCaptionButton)); - - public bool HasLeftCaptionButton - { - get => GetValue(HasLeftCaptionButtonProperty); - set => SetValue(HasLeftCaptionButtonProperty, value); + get => _captionHeight; + set => SetAndRaise(CaptionHeightProperty, ref _captionHeight, value); } public bool HasRightCaptionButton @@ -44,19 +35,11 @@ public bool HasRightCaptionButton public Launcher() { if (OperatingSystem.IsMacOS()) - { - HasLeftCaptionButton = true; CaptionHeight = new GridLength(34); - ExtendClientAreaChromeHints |= ExtendClientAreaChromeHints.OSXThickTitleBar; - } else if (UseSystemWindowFrame) - { CaptionHeight = new GridLength(30); - } else - { CaptionHeight = new GridLength(38); - } InitializeComponent(); PositionChanged += OnPositionChanged; @@ -99,9 +82,7 @@ protected override void OnOpened(EventArgs e) base.OnOpened(e); var preferences = ViewModels.Preferences.Instance; - var state = preferences.Layout.LauncherWindowState; - if (state == WindowState.Maximized || state == WindowState.FullScreen) - WindowState = WindowState.Maximized; + WindowState = preferences.Layout.LauncherWindowState; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -113,9 +94,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang var state = (WindowState)change.NewValue!; _lastWindowState = (WindowState)change.OldValue!; - if (OperatingSystem.IsMacOS()) - HasLeftCaptionButton = state != WindowState.FullScreen; - else if (!UseSystemWindowFrame) + if (!OperatingSystem.IsMacOS() && !UseSystemWindowFrame) CaptionHeight = new GridLength(state == WindowState.Maximized ? 30 : 38); ViewModels.Preferences.Instance.Layout.LauncherWindowState = state; @@ -125,6 +104,14 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (!IsActive && DataContext is ViewModels.Launcher { CommandPalette: { } } vm) vm.CommandPalette = null; } + + if (OperatingSystem.IsMacOS() && WindowState != WindowState.FullScreen) + { + if (change.Property == WindowStateProperty || + change.Property == BoundsProperty || + change.Property == TitleProperty) + Native.MacOSUtilities.AdjustTrafficLightsForThickTitleBar(this); + } } protected override void OnSizeChanged(SizeChangedEventArgs e) @@ -156,7 +143,9 @@ protected override async void OnKeyDown(KeyEventArgs e) } // Register hotkeys for Windows/Linux (macOS has registered these keys in system menu bar) - if (!OperatingSystem.IsMacOS()) + var isMacOS = OperatingSystem.IsMacOS(); + var cmdKey = isMacOS ? KeyModifiers.Meta : KeyModifiers.Control; + if (!isMacOS) { if (e is { KeyModifiers: KeyModifiers.Control, Key: Key.OemComma }) { @@ -179,7 +168,17 @@ protected override async void OnKeyDown(KeyEventArgs e) } } - var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + // Ctrl+` to open terminal. On macOS, Cmd+` is used to switch between windows + if (e is { Key: Key.OemTilde, KeyModifiers: KeyModifiers.Control }) + { + if (vm.ActivePage.Data is ViewModels.Repository repo) + Native.OS.OpenTerminal(repo.FullPath); + else + ViewModels.Welcome.Instance.OpenTerminal(); + + e.Handled = true; + return; + } if (vm.CommandPalette != null) { @@ -209,7 +208,7 @@ protected override async void OnKeyDown(KeyEventArgs e) return; } - if (e.Key == Key.N) + if (e.Key == Key.R) { if (vm.ActivePage.Data is not ViewModels.Welcome) vm.AddNewTab(); @@ -219,7 +218,7 @@ protected override async void OnKeyDown(KeyEventArgs e) return; } - if (e.Key == Key.O && e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + if (e.Key == Key.L) { if (vm.ActivePage.Data is not ViewModels.Welcome) vm.AddNewTab(); @@ -229,23 +228,23 @@ protected override async void OnKeyDown(KeyEventArgs e) return; } - if (e.Key == Key.T) + if (e.Key == Key.T && e.KeyModifiers == cmdKey) { vm.AddNewTab(); e.Handled = true; return; } - if ((OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Alt) && e.Key == Key.Right) || - (!OperatingSystem.IsMacOS() && !e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.Tab)) + if ((isMacOS && e.KeyModifiers.HasFlag(KeyModifiers.Alt) && e.Key == Key.Right) || + (!isMacOS && !e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.Tab)) { vm.GotoNextTab(); e.Handled = true; return; } - if ((OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Alt) && e.Key == Key.Left) || - (!OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.Tab)) + if ((isMacOS && e.KeyModifiers.HasFlag(KeyModifiers.Alt) && e.Key == Key.Left) || + (!isMacOS && e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.Tab)) { vm.GotoPrevTab(); e.Handled = true; @@ -268,37 +267,35 @@ protected override async void OnKeyDown(KeyEventArgs e) repo.SelectedViewIndex = 2; e.Handled = true; return; + case Key.P when e.KeyModifiers.HasFlag(KeyModifiers.Shift): + vm.CommandPalette = new ViewModels.RepositoryCommandPalette(repo); + e.Handled = true; + return; case Key.F when e.KeyModifiers.HasFlag(KeyModifiers.Shift): - repo.IsSearchingCommits = true; + repo.Histories.IsSearchingCommits = !repo.Histories.IsSearchingCommits; e.Handled = true; return; - case Key.H when e.KeyModifiers.HasFlag(KeyModifiers.Shift): - repo.IsSearchingCommits = false; + case Key.B when e.KeyModifiers.HasFlag(KeyModifiers.Shift): + if (repo.CanCreatePopup() && repo.GetSelectedCommitInHistory() is { } bc) + repo.ShowPopup(new ViewModels.CreateBranch(repo, bc)); e.Handled = true; return; - case Key.P when e.KeyModifiers.HasFlag(KeyModifiers.Shift): - vm.CommandPalette = new ViewModels.RepositoryCommandPalette(repo); + case Key.T when e.KeyModifiers.HasFlag(KeyModifiers.Shift): + if (repo.CanCreatePopup() && repo.GetSelectedCommitInHistory() is { } tc) + repo.ShowPopup(new ViewModels.CreateTag(repo, tc)); e.Handled = true; return; - } - } - else - { - var welcome = this.FindDescendantOfType(); - if (welcome != null) - { - if (e.Key == Key.F) - { - welcome.SearchBox.Focus(); + case Key.E: + Native.OS.OpenInFileManager(repo.FullPath); e.Handled = true; return; - } } } } else if (e.Key == Key.Escape) { vm.ActivePage.CancelPopup(); + vm.ActivePage.Notifications.Clear(); e.Handled = true; return; } @@ -321,6 +318,19 @@ protected override async void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); } + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (!e.Handled) + { + if (e.Properties.PointerUpdateKind == PointerUpdateKind.XButton1Pressed) + (DataContext as ViewModels.Launcher)?.GotoPrevTab(); + else if (e.Properties.PointerUpdateKind == PointerUpdateKind.XButton2Pressed) + (DataContext as ViewModels.Launcher)?.GotoNextTab(); + } + } + protected override void OnClosing(WindowClosingEventArgs e) { base.OnClosing(e); @@ -423,6 +433,22 @@ private void OnCloseCommandPalette(object sender, PointerPressedEventArgs e) e.Handled = true; } + private async void OnShowNewVersion(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Launcher { NewVersion: { } ver } vm) + { + vm.NewVersion = null; + + var ctx = new ViewModels.SelfUpdate { Data = ver }; + var dialog = new SelfUpdate() { DataContext = ctx }; + await dialog.ShowDialog(this); + } + + e.Handled = true; + } + + private GridLength _captionHeight = new(32); private WindowState _lastWindowState = WindowState.Normal; } } + diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index 8a99ac0253..08107bfa90 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -9,12 +9,10 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.LauncherPage" x:DataType="vm:LauncherPage"> - + @@ -31,14 +29,38 @@ - + - + - + + + + + + + + + + + + + + + + + + + + + @@ -59,64 +81,88 @@ - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + + + + + + + + + @@ -157,7 +201,7 @@ diff --git a/src/Views/LauncherPage.axaml.cs b/src/Views/LauncherPage.axaml.cs index 74e82aae14..8981a86c29 100644 --- a/src/Views/LauncherPage.axaml.cs +++ b/src/Views/LauncherPage.axaml.cs @@ -31,11 +31,7 @@ private async void OnPopupSureByHotKey(object sender, RoutedEventArgs e) PhysicalKey = PhysicalKey.Enter, }; - if (control is AvaloniaEdit.TextEditor editor) - editor.TextArea.TextView.RaiseEvent(fake); - else - control.RaiseEvent(fake); - + control.RaiseEvent(fake); e.Handled = false; return; } @@ -63,17 +59,17 @@ private void OnPopupCancel(object _, RoutedEventArgs e) e.Handled = true; } - private void OnMaskClicked(object sender, PointerPressedEventArgs e) + private void OnPopupTerminate(object _, RoutedEventArgs e) { - OnPopupCancel(sender, e); + if (DataContext is ViewModels.LauncherPage page) + page.TerminatePopup(); + + e.Handled = true; } - private async void OnCopyNotification(object sender, RoutedEventArgs e) + private void OnMaskClicked(object sender, PointerPressedEventArgs e) { - if (sender is Button { DataContext: Models.Notification notice }) - await this.CopyTextAsync(notice.Message); - - e.Handled = true; + OnPopupCancel(sender, e); } private void OnDismissNotification(object sender, RoutedEventArgs e) diff --git a/src/Views/LauncherPagesCommandPalette.axaml b/src/Views/LauncherPagesCommandPalette.axaml index f593a3e523..4acb43a2a8 100644 --- a/src/Views/LauncherPagesCommandPalette.axaml +++ b/src/Views/LauncherPagesCommandPalette.axaml @@ -84,13 +84,11 @@ - - + + + + + + + + + + + + + + + + + + + + + @@ -157,11 +188,9 @@ - + + IsVisible="{Binding #ThisControl.IsScrollButtonVisible}"> @@ -29,12 +29,30 @@ PointerWheelChanged="ScrollTabs"> + + + + + + + @@ -64,12 +82,11 @@ - + + IsVisible="{Binding !#ThisControl.IsScrollButtonVisible}"> @@ -148,7 +165,7 @@ - + diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index a7ec52481a..19bb5967ac 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -12,13 +12,16 @@ namespace SourceGit.Views { public class LauncherTabSizeBox : Border { - public static readonly StyledProperty UseFixedWidthProperty = - AvaloniaProperty.Register(nameof(UseFixedWidth), true); + public static readonly DirectProperty UseFixedWidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(UseFixedWidth), + static o => o.UseFixedWidth, + static (o, v) => o.UseFixedWidth = v); public bool UseFixedWidth { - get => GetValue(UseFixedWidthProperty); - set => SetValue(UseFixedWidthProperty, value); + get => _useFixedWidth; + set => SetAndRaise(UseFixedWidthProperty, ref _useFixedWidth, value); } public LauncherTabSizeBox() @@ -34,23 +37,27 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == UseFixedWidthProperty) { - if (UseFixedWidth) + if (_useFixedWidth) Width = 200; else Width = double.NaN; } } + + private bool _useFixedWidth = true; } public partial class LauncherTabBar : UserControl { - public static readonly StyledProperty IsScrollerVisibleProperty = - AvaloniaProperty.Register(nameof(IsScrollerVisible)); + public static readonly DirectProperty IsScrollButtonVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsScrollButtonVisible), + static o => o.IsScrollButtonVisible); - public bool IsScrollerVisible + public bool IsScrollButtonVisible { - get => GetValue(IsScrollerVisibleProperty); - set => SetValue(IsScrollerVisibleProperty, value); + get => _isScrollButtonVisible; + set => SetAndRaise(IsScrollButtonVisibleProperty, ref _isScrollButtonVisible, value); } public LauncherTabBar() @@ -74,7 +81,7 @@ public override void Render(DrawingContext context) var separatorPen = new Pen(new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? Colors.White : Colors.Black, 0.2)); var separatorY = (height - 18) * 0.5 + 1; - if (!IsScrollerVisible && selectedIdx > 0) + if (!_isScrollButtonVisible && selectedIdx > 0) { var container = LauncherTabsList.ContainerFromIndex(0); if (container != null) @@ -97,7 +104,7 @@ public override void Render(DrawingContext context) if (containerEndX < startX || containerEndX > endX) continue; - if (IsScrollerVisible && i == count - 1) + if (_isScrollButtonVisible && i == count - 1) break; var separatorX = containerEndX - startX + LauncherTabsScroller.Bounds.X - 0.5; @@ -164,31 +171,39 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang private void ScrollTabs(object _, PointerWheelEventArgs e) { - if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + if (Math.Abs(e.Delta.X) < Math.Abs(e.Delta.Y)) { - if (e.Delta.Y < 0) - LauncherTabsScroller.LineRight(); - else if (e.Delta.Y > 0) - LauncherTabsScroller.LineLeft(); + var x = LauncherTabsScroller.Offset.X; + var extent = LauncherTabsScroller.Extent.Width; + var viewport = LauncherTabsScroller.Viewport.Width; + var delta = e.Delta.Y; + + if (extent > viewport) + { + x += -delta * 64; // Use the same logic with vertical scrolling in `ScrollContentPresenter` + x = Math.Min(Math.Max(x, 0), extent - viewport); + } + + LauncherTabsScroller.Offset = new Vector(x, 0); e.Handled = true; } } private void ScrollTabsLeft(object _, RoutedEventArgs e) { - LauncherTabsScroller.LineLeft(); + LauncherTabsScroller.Offset -= _scrollStep; e.Handled = true; } private void ScrollTabsRight(object _, RoutedEventArgs e) { - LauncherTabsScroller.LineRight(); + LauncherTabsScroller.Offset += _scrollStep; e.Handled = true; } private void OnTabsLayoutUpdated(object _1, EventArgs _2) { - SetCurrentValue(IsScrollerVisibleProperty, LauncherTabsScroller.Extent.Width > LauncherTabsScroller.Viewport.Width); + IsScrollButtonVisible = LauncherTabsScroller.Extent.Width > LauncherTabsScroller.Viewport.Width; InvalidateVisual(); } @@ -207,26 +222,30 @@ private void OnPointerPressedTab(object sender, PointerPressedEventArgs e) (DataContext as ViewModels.Launcher)?.CloseTab(page); e.Handled = true; } + else if (point.Properties.IsLeftButtonPressed) + { + _pressedTabEvent = e; + _startDragTab = false; + } else { - _pressedTab = true; + _pressedTabEvent = null; _startDragTab = false; - _pressedTabPosition = e.GetPosition(border); } } } private void OnPointerReleasedTab(object _1, PointerReleasedEventArgs _2) { - _pressedTab = false; + _pressedTabEvent = null; _startDragTab = false; } private async void OnPointerMovedOverTab(object sender, PointerEventArgs e) { - if (_pressedTab && !_startDragTab && sender is Border { DataContext: ViewModels.LauncherPage page } border) + if (_pressedTabEvent != null && !_startDragTab && sender is Border { DataContext: ViewModels.LauncherPage page } border) { - var delta = e.GetPosition(border) - _pressedTabPosition; + var delta = e.GetPosition(border) - _pressedTabEvent.GetPosition(border); var sizeSquired = delta.X * delta.X + delta.Y * delta.Y; if (sizeSquired < 64) return; @@ -235,7 +254,7 @@ private async void OnPointerMovedOverTab(object sender, PointerEventArgs e) var data = new DataTransfer(); data.Add(DataTransferItem.Create(_dndMainTabFormat, page.Node.Id)); - await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move); + await DragDrop.DoDragDropAsync(_pressedTabEvent, data, DragDropEffects.Move); } e.Handled = true; } @@ -269,7 +288,7 @@ private void DropTab(object sender, DragEventArgs e) launcher.MoveTab(target, to); - _pressedTab = false; + _pressedTabEvent = null; _startDragTab = false; e.Handled = true; } @@ -283,6 +302,17 @@ private void OnTabContextRequested(object sender, ContextRequestedEventArgs e) if (vm.ActivePage.Data is ViewModels.Repository repo) { + var explore = new MenuItem(); + explore.Header = App.Text("Repository.Explore"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.Tag = OperatingSystem.IsMacOS() ? "⌘+E" : "Ctrl+E"; + explore.Click += (_, e) => + { + Native.OS.OpenInFileManager(repo.FullPath); + e.Handled = true; + }; + menu.Items.Add(explore); + var refresh = new MenuItem(); refresh.Header = App.Text("PageTabBar.Tab.Refresh"); refresh.Icon = this.CreateMenuIcon("Icons.Loading"); @@ -309,10 +339,10 @@ private void OnTabContextRequested(object sender, ContextRequestedEventArgs e) var edit = new MenuItem(); edit.Header = App.Text("PageTabBar.Tab.Edit"); edit.Icon = this.CreateMenuIcon("Icons.Edit"); - edit.Click += (_, e) => + edit.Click += (_, ev) => { page.Node.Edit(); - e.Handled = true; + ev.Handled = true; }; menu.Items.Add(edit); @@ -393,8 +423,9 @@ private void OnCloseTab(object sender, RoutedEventArgs e) e.Handled = true; } - private bool _pressedTab = false; - private Point _pressedTabPosition = new(); + private bool _isScrollButtonVisible = false; + private readonly Vector _scrollStep = new(64, 0); + private PointerPressedEventArgs _pressedTabEvent = null; private bool _startDragTab = false; private readonly DataFormat _dndMainTabFormat = DataFormat.CreateStringApplicationFormat("sourcegit-dnd-main-tab"); } diff --git a/src/Views/ListBoxEx.cs b/src/Views/ListBoxEx.cs index 2d135a90cd..0969d2d6a7 100644 --- a/src/Views/ListBoxEx.cs +++ b/src/Views/ListBoxEx.cs @@ -15,5 +15,12 @@ protected override void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); } + + protected void Select(object item) + { + SelectedItem = item; + ScrollIntoView(item); + ContainerFromItem(item)?.Focus(); + } } } diff --git a/src/Views/MacOSTrafficLightsSpacer.cs b/src/Views/MacOSTrafficLightsSpacer.cs new file mode 100644 index 0000000000..f74f86d19f --- /dev/null +++ b/src/Views/MacOSTrafficLightsSpacer.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia; +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public class MacOSTrafficLightsSpacer : Control + { + public static readonly DirectProperty ZoomProperty = + AvaloniaProperty.RegisterDirect( + nameof(Zoom), + static o => o.Zoom, + static (o, v) => o.Zoom = v); + + public double Zoom + { + get => _zoom; + set => SetAndRaise(ZoomProperty, ref _zoom, value); + } + + public static readonly DirectProperty IsFullScreenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsFullScreen), + static o => o.IsFullScreen, + static (o, v) => o.IsFullScreen = v); + + public bool IsFullScreen + { + get => _isFullScreen; + set => SetAndRaise(IsFullScreenProperty, ref _isFullScreen, value); + } + + public MacOSTrafficLightsSpacer() + { + IsHitTestVisible = false; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ZoomProperty || change.Property == IsFullScreenProperty) + InvalidateMeasure(); + } + + protected override Size MeasureOverride(Size availableSize) + { + return _isFullScreen ? new Size(4, 24) : new Size(76.0 / Math.Max(_zoom, 1.0), 24.0); + } + + private double _zoom = 1.0; + private bool _isFullScreen = false; + } +} diff --git a/src/Views/Merge.axaml b/src/Views/Merge.axaml index 5ceb93ff78..48cc9b7111 100644 --- a/src/Views/Merge.axaml +++ b/src/Views/Merge.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Merge" @@ -18,8 +19,8 @@ Classes="bold" Text="{DynamicResource Text.Merge}"/> - - + + - - + + @@ -64,7 +74,8 @@ Margin="0,0,8,0" Text="{DynamicResource Text.Merge.Mode}"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/MergeConflictEditor.axaml b/src/Views/MergeConflictEditor.axaml index 8ef06b94ce..e5b4379661 100644 --- a/src/Views/MergeConflictEditor.axaml +++ b/src/Views/MergeConflictEditor.axaml @@ -13,7 +13,7 @@ Icon="/App.ico" Title="{DynamicResource Text.MergeConflictEditor.Title}" MinWidth="1024" MinHeight="600"> - + - - - - - - + + - - - - - - - - - + - - - + + + + + @@ -103,6 +95,7 @@ VerticalAlignment="Center" UseGraphColor="False"/> + + + + + + + + + + + @@ -309,7 +315,7 @@ - + - + + + @@ -891,9 +902,28 @@ + + + + + + + + + + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 6ab603f02b..773a0f3e7b 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -11,6 +11,28 @@ namespace SourceGit.Views { public partial class Preferences : ChromelessWindow { + public static readonly DirectProperty GitVersionProperty = + AvaloniaProperty.RegisterDirect( + nameof(GitVersion), + static o => o.GitVersion); + + public string GitVersion + { + get => _gitVersion; + set => SetAndRaise(GitVersionProperty, ref _gitVersion, value); + } + + public static readonly DirectProperty ShowGitVersionWarningProperty = + AvaloniaProperty.RegisterDirect( + nameof(ShowGitVersionWarning), + static o => o.ShowGitVersionWarning); + + public bool ShowGitVersionWarning + { + get => _showGitVersionWarning; + set => SetAndRaise(ShowGitVersionWarningProperty, ref _showGitVersionWarning, value); + } + public string DefaultUser { get; @@ -35,24 +57,6 @@ public bool EnablePruneOnFetch set; } - public static readonly StyledProperty GitVersionProperty = - AvaloniaProperty.Register(nameof(GitVersion)); - - public string GitVersion - { - get => GetValue(GitVersionProperty); - set => SetValue(GitVersionProperty, value); - } - - public static readonly StyledProperty ShowGitVersionWarningProperty = - AvaloniaProperty.Register(nameof(ShowGitVersionWarning)); - - public bool ShowGitVersionWarning - { - get => GetValue(ShowGitVersionWarningProperty); - set => SetValue(ShowGitVersionWarningProperty, value); - } - public bool EnableGPGCommitSigning { get; @@ -65,22 +69,28 @@ public bool EnableGPGTagSigning set; } - public static readonly StyledProperty GPGFormatProperty = - AvaloniaProperty.Register(nameof(GPGFormat), Models.GPGFormat.Supported[0]); + public static readonly DirectProperty GPGFormatProperty = + AvaloniaProperty.RegisterDirect( + nameof(GPGFormat), + static o => o.GPGFormat, + static (o, v) => o.GPGFormat = v); public Models.GPGFormat GPGFormat { - get => GetValue(GPGFormatProperty); - set => SetValue(GPGFormatProperty, value); + get => _gpgFormat; + set => SetAndRaise(GPGFormatProperty, ref _gpgFormat, value); } - public static readonly StyledProperty GPGExecutableFileProperty = - AvaloniaProperty.Register(nameof(GPGExecutableFile)); + public static readonly DirectProperty GPGExecutableFileProperty = + AvaloniaProperty.RegisterDirect( + nameof(GPGExecutableFile), + static o => o.GPGExecutableFile, + static (o, v) => o.GPGExecutableFile = v); public string GPGExecutableFile { - get => GetValue(GPGExecutableFileProperty); - set => SetValue(GPGExecutableFileProperty, value); + get => _gpgExecutableFile; + set => SetAndRaise(GPGExecutableFileProperty, ref _gpgExecutableFile, value); } public string GPGUserKey @@ -95,22 +105,28 @@ public bool EnableHTTPSSLVerify set; } = false; - public static readonly StyledProperty SelectedOpenAIServiceProperty = - AvaloniaProperty.Register(nameof(SelectedOpenAIService)); + public static readonly DirectProperty SelectedOpenAIServiceProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedOpenAIService), + static o => o.SelectedOpenAIService, + static (o, v) => o.SelectedOpenAIService = v); public AI.Service SelectedOpenAIService { - get => GetValue(SelectedOpenAIServiceProperty); - set => SetValue(SelectedOpenAIServiceProperty, value); + get => _selectedOpenAIService; + set => SetAndRaise(SelectedOpenAIServiceProperty, ref _selectedOpenAIService, value); } - public static readonly StyledProperty SelectedCustomActionProperty = - AvaloniaProperty.Register(nameof(SelectedCustomAction)); + public static readonly DirectProperty SelectedCustomActionProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedCustomAction), + static o => o.SelectedCustomAction, + static (o, v) => o.SelectedCustomAction = v); public Models.CustomAction SelectedCustomAction { - get => GetValue(SelectedCustomActionProperty); - set => SetValue(SelectedCustomActionProperty, value); + get => _selectedCustomAction; + set => SetAndRaise(SelectedCustomActionProperty, ref _selectedCustomAction, value); } public Preferences() @@ -232,6 +248,12 @@ private async void SelectThemeOverrideFile(object _, RoutedEventArgs e) e.Handled = true; } + private void OpenThemeRepository(object _, RoutedEventArgs e) + { + Native.OS.OpenBrowser($"https://github.com/sourcegit-scm/sourcegit-theme"); + e.Handled = true; + } + private async void SelectGitExecutable(object _, RoutedEventArgs e) { var pattern = OperatingSystem.IsWindows() ? "git.exe" : "git"; @@ -319,7 +341,11 @@ private async void SelectShellOrTerminal(object _, RoutedEventArgs e) { options = new FilePickerOpenOptions() { - FileTypeFilter = [new FilePickerFileType(shell.Name) { Patterns = [shell.Exec] }], + FileTypeFilter = [new FilePickerFileType(shell.Name) + { + Patterns = shell.Exec.Split('|', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + }], AllowMultiple = false, }; } @@ -429,8 +455,8 @@ private async void SelectExecutableForCustomAction(object sender, RoutedEventArg { var options = new FilePickerOpenOptions() { - FileTypeFilter = [new FilePickerFileType("Executable file(script)") { Patterns = ["*.*"] }], AllowMultiple = false, + FileTypeFilter = [new("Executable file(script)") { Patterns = ["*"] }] }; try @@ -495,5 +521,12 @@ private void UpdateGitVersion() GitVersion = Native.OS.GitVersionString; ShowGitVersionWarning = !string.IsNullOrEmpty(GitVersion) && Native.OS.GitVersion < Models.GitVersions.MINIMAL; } + + private string _gitVersion = string.Empty; + private bool _showGitVersionWarning = false; + private Models.GPGFormat _gpgFormat = Models.GPGFormat.Supported[0]; + private string _gpgExecutableFile = string.Empty; + private AI.Service _selectedOpenAIService = null; + private Models.CustomAction _selectedCustomAction = null; } } diff --git a/src/Views/PruneRemote.axaml b/src/Views/PruneRemote.axaml index 0484c76dc5..10e99d3cb1 100644 --- a/src/Views/PruneRemote.axaml +++ b/src/Views/PruneRemote.axaml @@ -16,7 +16,7 @@ Classes="bold" Text="{DynamicResource Text.PruneRemote}"/> - + diff --git a/src/Views/PruneWorktrees.axaml b/src/Views/PruneWorktrees.axaml index b6e45d6116..a7f6f3f44f 100644 --- a/src/Views/PruneWorktrees.axaml +++ b/src/Views/PruneWorktrees.axaml @@ -14,7 +14,7 @@ Classes="bold" Text="{DynamicResource Text.PruneWorktrees}"/> - + diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index 8c091b6912..d5a4eb3a71 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -18,7 +18,7 @@ Classes="bold" Text="{DynamicResource Text.Pull.Title}"/> - + - - - - - - - - - - + @@ -18,28 +19,17 @@ Text="{DynamicResource Text.Push.Title}"/> - + - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}" + SelectionChanged="OnTagsSelectionChanged" + RowsChanged="OnLeftSidebarRowsChanged" + SearchRequested="OnToggleSidebarFilter"/> + + + + + + + + - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + - + + + + + + - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index d75d92f6a3..93d77396ec 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -4,13 +4,81 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { public partial class Repository : UserControl { + public static readonly DirectProperty IsCommitSearchPanelVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsCommitSearchPanelVisible), + static o => o.IsCommitSearchPanelVisible, + static (o, v) => o.IsCommitSearchPanelVisible = v); + + public bool IsCommitSearchPanelVisible + { + get => _isCommitSearchPanelVisible; + set + { + if (SetAndRaise(IsCommitSearchPanelVisibleProperty, ref _isCommitSearchPanelVisible, value)) + CalculateSidebarWidth(); + } + } + + public static readonly DirectProperty IsNormalSidebarVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsNormalSidebarVisible), + static o => o.IsNormalSidebarVisible, + static (o, v) => o.IsNormalSidebarVisible = v); + + public bool IsNormalSidebarVisible + { + get => _isNormalSidebarVisible; + set + { + if (SetAndRaise(IsNormalSidebarVisibleProperty, ref _isNormalSidebarVisible, value)) + CalculateSidebarWidth(); + } + } + + public static readonly DirectProperty SidebarMinWidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(SidebarMinWidth), + static o => o.SidebarMinWidth, + static (o, v) => o.SidebarMinWidth = v); + + public double SidebarMinWidth + { + get => _sidebarMinWidth; + set => SetAndRaise(SidebarMinWidthProperty, ref _sidebarMinWidth, value); + } + + public static readonly DirectProperty SidebarWidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(SidebarWidth), + static o => o.SidebarWidth, + static (o, v) => o.SidebarWidth = v); + + public GridLength SidebarWidth + { + get => _sidebarWidth; + set + { + if (SetAndRaise(SidebarWidthProperty, ref _sidebarWidth, value)) + { + var layout = ViewModels.Preferences.Instance.Layout; + if (_isCommitSearchPanelVisible) + layout.RepositorySearchCommitWidth = value.Value; + else if (_isNormalSidebarVisible) + layout.RepositorySidebarWidth = value.Value; + } + } + } + public Repository() { + _sidebarWidth = new GridLength(ViewModels.Preferences.Instance.Layout.RepositorySidebarWidth); InitializeComponent(); } @@ -20,51 +88,42 @@ protected override void OnLoaded(RoutedEventArgs e) UpdateLeftSidebarLayout(); } - private void OnToggleFilter(object _, RoutedEventArgs e) + private async void OnOpenConfigure(object sender, RoutedEventArgs e) { - FilterBox.Focus(); - e.Handled = true; + if (DataContext is ViewModels.Repository repo) + { + await this.ShowDialogAsync(new ViewModels.RepositoryConfigure(repo)); + e.Handled = true; + } } - private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private async void OnSkipInProgress(object sender, RoutedEventArgs e) { - if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true }) - TxtSearchCommitsBox.Focus(); + if (DataContext is ViewModels.Repository repo) + await repo.SkipMergeAsync(); + + e.Handled = true; } - private void OnSearchKeyDown(object _, KeyEventArgs e) + private void OnResolveInProgress(object sender, RoutedEventArgs e) { - if (DataContext is not ViewModels.Repository repo) - return; - - if (e.Key == Key.Enter) - { - repo.SearchCommitContext.StartSearch(); - e.Handled = true; - } - else if (e.Key == Key.Down) - { - if (repo.SearchCommitContext.Suggestions is { Count: > 0 }) - { - SearchSuggestionBox.Focus(NavigationMethod.Tab); - SearchSuggestionBox.SelectedIndex = 0; - } + if (DataContext is ViewModels.Repository repo) + repo.SelectedViewIndex = 1; - e.Handled = true; - } - else if (e.Key == Key.Escape) - { - repo.SearchCommitContext.ClearSuggestions(); - e.Handled = true; - } + e.Handled = true; } - private void OnClearSearchCommitFilter(object _, RoutedEventArgs e) + private async void OnAbortInProgress(object sender, RoutedEventArgs e) { - if (DataContext is not ViewModels.Repository repo) - return; + if (DataContext is ViewModels.Repository repo) + await repo.AbortMergeAsync(); + + e.Handled = true; + } - repo.SearchCommitContext.ClearFilter(); + private void OnToggleSidebarFilter(object _, RoutedEventArgs e) + { + FilterBox.Focus(); e.Handled = true; } @@ -185,11 +244,10 @@ private void OnLeftSidebarSizeChanged(object _, SizeChangedEventArgs e) private void UpdateLeftSidebarLayout() { - var vm = DataContext as ViewModels.Repository; - if (vm?.Settings == null) + if (!IsLoaded || _isCommitSearchPanelVisible) return; - if (!IsLoaded) + if (DataContext is not ViewModels.Repository { UIStates: { } } vm) return; var leftHeight = LeftSidebarGroups.Bounds.Height - 28.0 * 5 - 4; @@ -306,154 +364,6 @@ private void UpdateLeftSidebarLayout() } } - private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e) - { - if (DataContext is not ViewModels.Repository repo) - return; - - if (e.Key == Key.Escape) - { - repo.SearchCommitContext.ClearSuggestions(); - e.Handled = true; - } - else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content) - { - repo.SearchCommitContext.Filter = content; - TxtSearchCommitsBox.CaretIndex = content.Length; - repo.SearchCommitContext.StartSearch(); - e.Handled = true; - } - } - - private void OnSearchSuggestionDoubleTapped(object sender, TappedEventArgs e) - { - if (DataContext is not ViewModels.Repository repo) - return; - - var content = (sender as StackPanel)?.DataContext as string; - if (!string.IsNullOrEmpty(content)) - { - repo.SearchCommitContext.Filter = content; - TxtSearchCommitsBox.CaretIndex = content.Length; - repo.SearchCommitContext.StartSearch(); - } - e.Handled = true; - } - - private void OnOpenAdvancedHistoriesOption(object sender, RoutedEventArgs e) - { - if (sender is Button button && DataContext is ViewModels.Repository repo) - { - var pref = ViewModels.Preferences.Instance; - - var layout = new MenuItem(); - layout.Header = App.Text("Repository.HistoriesLayout"); - layout.IsEnabled = false; - - var isHorizontal = pref.UseTwoColumnsLayoutInHistories; - var horizontal = new MenuItem(); - horizontal.Header = App.Text("Repository.HistoriesLayout.Horizontal"); - if (isHorizontal) - horizontal.Icon = this.CreateMenuIcon("Icons.Check"); - horizontal.Click += (_, ev) => - { - pref.UseTwoColumnsLayoutInHistories = true; - ev.Handled = true; - }; - - var vertical = new MenuItem(); - vertical.Header = App.Text("Repository.HistoriesLayout.Vertical"); - if (!isHorizontal) - vertical.Icon = this.CreateMenuIcon("Icons.Check"); - vertical.Click += (_, ev) => - { - pref.UseTwoColumnsLayoutInHistories = false; - ev.Handled = true; - }; - - var showFlags = new MenuItem(); - showFlags.Header = App.Text("Repository.ShowFlags"); - showFlags.IsEnabled = false; - - var reflog = new MenuItem(); - reflog.Header = App.Text("Repository.ShowLostCommits"); - reflog.Tag = "--reflog"; - if (repo.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.Reflog)) - reflog.Icon = this.CreateMenuIcon("Icons.Check"); - reflog.Click += (_, ev) => - { - repo.ToggleHistoryShowFlag(Models.HistoryShowFlags.Reflog); - ev.Handled = true; - }; - - var firstParentOnly = new MenuItem(); - firstParentOnly.Header = App.Text("Repository.ShowFirstParentOnly"); - firstParentOnly.Tag = "--first-parent"; - if (repo.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)) - firstParentOnly.Icon = this.CreateMenuIcon("Icons.Check"); - firstParentOnly.Click += (_, ev) => - { - repo.ToggleHistoryShowFlag(Models.HistoryShowFlags.FirstParentOnly); - ev.Handled = true; - }; - - var simplifyByDecoration = new MenuItem(); - simplifyByDecoration.Header = App.Text("Repository.ShowDecoratedCommitsOnly"); - simplifyByDecoration.Tag = "--simplify-by-decoration"; - if (repo.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) - simplifyByDecoration.Icon = this.CreateMenuIcon("Icons.Check"); - simplifyByDecoration.Click += (_, ev) => - { - repo.ToggleHistoryShowFlag(Models.HistoryShowFlags.SimplifyByDecoration); - ev.Handled = true; - }; - - var order = new MenuItem(); - order.Header = App.Text("Repository.HistoriesOrder"); - order.IsEnabled = false; - - var dateOrder = new MenuItem(); - dateOrder.Header = App.Text("Repository.HistoriesOrder.ByDate"); - dateOrder.Tag = "--date-order"; - if (!repo.EnableTopoOrderInHistory) - dateOrder.Icon = this.CreateMenuIcon("Icons.Check"); - dateOrder.Click += (_, ev) => - { - repo.EnableTopoOrderInHistory = false; - ev.Handled = true; - }; - - var topoOrder = new MenuItem(); - topoOrder.Header = App.Text("Repository.HistoriesOrder.Topo"); - topoOrder.Tag = "--topo-order"; - if (repo.EnableTopoOrderInHistory) - topoOrder.Icon = this.CreateMenuIcon("Icons.Check"); - topoOrder.Click += (_, ev) => - { - repo.EnableTopoOrderInHistory = true; - ev.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - menu.Items.Add(layout); - menu.Items.Add(horizontal); - menu.Items.Add(vertical); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(showFlags); - menu.Items.Add(reflog); - menu.Items.Add(firstParentOnly); - menu.Items.Add(simplifyByDecoration); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(order); - menu.Items.Add(dateOrder); - menu.Items.Add(topoOrder); - menu.Open(button); - } - - e.Handled = true; - } - private void OnOpenSortLocalBranchMenu(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.Repository repo) @@ -573,30 +483,6 @@ private async void OnPruneWorktrees(object sender, RoutedEventArgs e) e.Handled = true; } - private async void OnSkipInProgress(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - await repo.SkipMergeAsync(); - - e.Handled = true; - } - - private void OnResolveInProgress(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - repo.SelectedViewIndex = 1; - - e.Handled = true; - } - - private async void OnAbortInProgress(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - await repo.AbortMergeAsync(); - - e.Handled = true; - } - private void OnRemoveSelectedHistoryFilter(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.Repository repo && sender is Button { DataContext: Models.HistoryFilter filter }) @@ -614,5 +500,39 @@ private async void OnBisectCommand(object sender, RoutedEventArgs e) e.Handled = true; } + + private void OnRightPagePropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Control.IsVisibleProperty && sender is Control page) + { + var diffViewer = page.FindDescendantOfType(); + diffViewer?.ToggleHotkeyBindings(page.IsVisible); + } + } + + private void CalculateSidebarWidth() + { + var layout = ViewModels.Preferences.Instance.Layout; + if (_isCommitSearchPanelVisible) + { + SidebarMinWidth = 200; + SidebarWidth = new GridLength(layout.RepositorySearchCommitWidth, GridUnitType.Pixel); + } + else if (_isNormalSidebarVisible) + { + SidebarMinWidth = 200; + SidebarWidth = new GridLength(layout.RepositorySidebarWidth, GridUnitType.Pixel); + } + else + { + SidebarMinWidth = 0; + SidebarWidth = new GridLength(0, GridUnitType.Pixel); + } + } + + private bool _isNormalSidebarVisible = true; + private bool _isCommitSearchPanelVisible = false; + private double _sidebarMinWidth = 200; + private GridLength _sidebarWidth; } } diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index d08052fc51..8509df574e 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -44,7 +44,7 @@ - + + + diff --git a/src/Views/RepositoryConfigure.axaml.cs b/src/Views/RepositoryConfigure.axaml.cs index bc445d2a70..b543d9edf0 100644 --- a/src/Views/RepositoryConfigure.axaml.cs +++ b/src/Views/RepositoryConfigure.axaml.cs @@ -40,8 +40,8 @@ private async void SelectExecutableForCustomAction(object sender, RoutedEventArg { var options = new FilePickerOpenOptions() { - FileTypeFilter = [new FilePickerFileType("Executable file(script)") { Patterns = ["*.*"] }], AllowMultiple = false, + FileTypeFilter = [new("Executable file(script)") { Patterns = ["*"] }] }; var selected = await StorageProvider.OpenFilePickerAsync(options); diff --git a/src/Views/RepositoryToolbar.axaml b/src/Views/RepositoryToolbar.axaml index 72ebb9dc54..08ed8e83c5 100644 --- a/src/Views/RepositoryToolbar.axaml +++ b/src/Views/RepositoryToolbar.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RepositoryToolbar" x:DataType="vm:Repository"> @@ -18,11 +19,7 @@ - - @@ -45,8 +42,14 @@ - - + + + + + - - + + @@ -107,30 +127,29 @@ - + diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs index 72bb046235..c5ba572cad 100644 --- a/src/Views/RepositoryToolbar.axaml.cs +++ b/src/Views/RepositoryToolbar.axaml.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.IO; using Avalonia.Controls; using Avalonia.Input; @@ -21,16 +23,21 @@ private void OpenWithExternalTools(object sender, RoutedEventArgs ev) if (sender is Button button && DataContext is ViewModels.Repository repo) { var fullpath = repo.FullPath; + if (!Directory.Exists(fullpath)) + return; + + var isMacOS = OperatingSystem.IsMacOS(); var menu = new ContextMenu(); menu.Placement = PlacementMode.BottomEdgeAlignedLeft; RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality); RenderOptions.SetEdgeMode(menu, EdgeMode.Antialias); - RenderOptions.SetTextRenderingMode(menu, TextRenderingMode.Antialias); + RenderOptions.SetTextRenderingMode(menu, TextRenderingMode.SubpixelAntialias); var explore = new MenuItem(); explore.Header = App.Text("Repository.Explore"); explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.Tag = isMacOS ? "⌘+E" : "Ctrl+E"; explore.Click += (_, e) => { Native.OS.OpenInFileManager(fullpath); @@ -40,6 +47,7 @@ private void OpenWithExternalTools(object sender, RoutedEventArgs ev) var terminal = new MenuItem(); terminal.Header = App.Text("Repository.Terminal"); terminal.Icon = this.CreateMenuIcon("Icons.Terminal"); + terminal.Tag = isMacOS ? "Λ+`" : "Ctrl+`"; terminal.Click += (_, e) => { Native.OS.OpenTerminal(fullpath); @@ -63,7 +71,23 @@ private void OpenWithExternalTools(object sender, RoutedEventArgs ev) item.Icon = new Image { Width = 16, Height = 16, Source = dupTool.IconImage }; var options = dupTool.MakeLaunchOptions(fullpath); - if (options is { Count: > 0 }) + var count = (dupTool.SupportOpenFolder ? 1 : 0) + (options?.Count ?? 0); + if (count == 0) + continue; + + if (count == 1) + { + var args = fullpath.Quoted(); + if (options is { Count: 1 }) + args = options[0].Args; + + item.Click += (_, e) => + { + dupTool.Launch(args); + e.Handled = true; + }; + } + else { foreach (var opt in options) { @@ -78,23 +102,18 @@ private void OpenWithExternalTools(object sender, RoutedEventArgs ev) item.Items.Add(subItem); } - var openAsFolder = new MenuItem(); - openAsFolder.Header = App.Text("Repository.OpenAsFolder"); - openAsFolder.Click += (_, e) => + if (dupTool.SupportOpenFolder) { - dupTool.Launch(fullpath.Quoted()); - e.Handled = true; - }; - item.Items.Add(new MenuItem() { Header = "-" }); - item.Items.Add(openAsFolder); - } - else - { - item.Click += (_, e) => - { - dupTool.Launch(fullpath.Quoted()); - e.Handled = true; - }; + var open = new MenuItem(); + open.Header = App.Text("Repository.OpenAsFolder"); + open.Click += (_, e) => + { + dupTool.Launch(fullpath.Quoted()); + e.Handled = true; + }; + item.Items.Add(new MenuItem() { Header = "-" }); + item.Items.Add(open); + } } menu.Items.Add(item); @@ -143,15 +162,6 @@ private async void OpenStatistics(object _, RoutedEventArgs e) } } - private async void OpenConfigure(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - await this.ShowDialogAsync(new ViewModels.RepositoryConfigure(repo)); - e.Handled = true; - } - } - private async void Fetch(object sender, TappedEventArgs e) { if (DataContext is ViewModels.Repository repo) @@ -235,7 +245,6 @@ private void OpenGitFlowMenu(object sender, RoutedEventArgs ev) { var startFeature = new MenuItem(); startFeature.Header = App.Text("GitFlow.StartFeature"); - startFeature.Icon = this.CreateMenuIcon("Icons.GitFlow.Feature"); startFeature.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -245,7 +254,6 @@ private void OpenGitFlowMenu(object sender, RoutedEventArgs ev) var startRelease = new MenuItem(); startRelease.Header = App.Text("GitFlow.StartRelease"); - startRelease.Icon = this.CreateMenuIcon("Icons.GitFlow.Release"); startRelease.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -255,7 +263,6 @@ private void OpenGitFlowMenu(object sender, RoutedEventArgs ev) var startHotfix = new MenuItem(); startHotfix.Header = App.Text("GitFlow.StartHotfix"); - startHotfix.Icon = this.CreateMenuIcon("Icons.GitFlow.Hotfix"); startHotfix.Click += (_, e) => { if (repo.CanCreatePopup()) @@ -266,6 +273,22 @@ private void OpenGitFlowMenu(object sender, RoutedEventArgs ev) menu.Items.Add(startFeature); menu.Items.Add(startRelease); menu.Items.Add(startHotfix); + + var type = repo.CurrentBranch != null ? repo.GetGitFlowType(repo.CurrentBranch) : Models.GitFlowBranchType.None; + if (type != Models.GitFlowBranchType.None) + { + var finish = new MenuItem(); + finish.Header = App.Text("GitFlow.Finish", repo.CurrentBranch.Name); + finish.Icon = this.CreateMenuIcon("Icons.GitFlow.Finish"); + finish.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.GitFlowFinish(repo, repo.CurrentBranch, type)); + e.Handled = true; + }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(finish); + } } else { @@ -445,15 +468,6 @@ private async void StartBisect(object sender, RoutedEventArgs e) e.Handled = true; } - private async void Cleanup(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - await repo.CleanupAsync(); - e.Handled = true; - } - } - private void OpenCustomActionMenu(object sender, RoutedEventArgs ev) { if (DataContext is ViewModels.Repository repo && sender is Control control) @@ -499,14 +513,23 @@ private async void OpenGitLogs(object sender, RoutedEventArgs e) } } + private async void Cleanup(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + await repo.CleanupAsync(); + e.Handled = true; + } + } + private void NavigateToHead(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository { CurrentBranch: not null } repo) + if (DataContext is ViewModels.Repository { CurrentBranch: { } head } repo) { var repoView = TopLevel.GetTopLevel(this)?.FindDescendantOfType(); - repoView?.LocalBranchTree?.Select(repo.CurrentBranch); + repoView?.LocalBranchTree?.Select(head); - repo.NavigateToCommit(repo.CurrentBranch.Head); + repo.NavigateToCommit(head.Head); e.Handled = true; } } diff --git a/src/Views/Reset.axaml b/src/Views/Reset.axaml index ed6bf338cc..f521bae60b 100644 --- a/src/Views/Reset.axaml +++ b/src/Views/Reset.axaml @@ -18,7 +18,7 @@ Classes="bold" Text="{DynamicResource Text.Reset}"/> - + - - + + - + - - + + diff --git a/src/Views/Revert.axaml b/src/Views/Revert.axaml index 4ed827059d..84ef3a98e1 100644 --- a/src/Views/Revert.axaml +++ b/src/Views/Revert.axaml @@ -17,7 +17,7 @@ Classes="bold" Text="{DynamicResource Text.Revert}"/> - + - - + + - + + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=ChangeViewMode, Mode=TwoWay}"/> - - + + + + @@ -115,7 +124,7 @@ IsVisible="{Binding IsLoading}"/> - + @@ -132,7 +141,7 @@ Focusable="False"/> - + 0 } selected } vm && - sender is ChangeCollectionView view) + if (DataContext is ViewModels.RevisionCompare { ChangeSelection: { Count: > 0 } selection } vm) { var patch = new MenuItem(); patch.Header = App.Text("FileCM.SaveAsPatch"); @@ -41,7 +40,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e if (storageFile != null) { var saveTo = storageFile.Path.LocalPath; - await vm.SaveChangesAsPatchAsync(selected, saveTo); + await vm.SaveChangesAsPatchAsync(selection.Changes, saveTo); } } catch (Exception exception) @@ -52,37 +51,47 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e e.Handled = true; }; + var selectedSingleFolder = selection.IsSingleFolder; + var fullPathOfFolder = selectedSingleFolder ? vm.GetAbsPath(selection.SingleFolderPath) : null; + var relativePathOfFolder = selectedSingleFolder ? selection.SingleFolderPath : null; + var menu = new ContextMenu(); - if (selected.Count == 1) + if (selection.Count == 1) { - var change = selected[0]; + var change = selection.Changes[0]; var changeFullPath = vm.GetAbsPath(change.Path); - 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) => + if (!selection.HasFolder) { - vm.OpenChangeWithExternalDiffTool(change); - ev.Handled = true; - }; - menu.Items.Add(openWithMerger); + 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.OpenChangeWithExternalDiffTool(change); + ev.Handled = true; + }; + menu.Items.Add(openWithMerger); + } - if (change.Index != Models.ChangeState.Deleted) + if (!selection.HasFolder || selectedSingleFolder) { var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = this.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(changeFullPath); + explore.IsEnabled = selectedSingleFolder ? Directory.Exists(fullPathOfFolder) : File.Exists(changeFullPath); explore.Click += (_, ev) => { - Native.OS.OpenInFileManager(changeFullPath); + Native.OS.OpenInFileManager(selectedSingleFolder ? fullPathOfFolder : changeFullPath); ev.Handled = true; }; menu.Items.Add(explore); } + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem() { Header = "-" }); + var resetToLeft = new MenuItem(); resetToLeft.Header = App.Text("ChangeCM.ResetFileTo", vm.LeftSideDesc); resetToLeft.Icon = this.CreateMenuIcon("Icons.File.Checkout"); @@ -109,7 +118,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - await this.CopyTextAsync(change.Path); + await this.CopyTextAsync(selectedSingleFolder ? relativePathOfFolder : change.Path); ev.Handled = true; }; @@ -119,11 +128,10 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, ev) => { - await this.CopyTextAsync(changeFullPath); + await this.CopyTextAsync(selectedSingleFolder ? fullPathOfFolder : changeFullPath); ev.Handled = true; }; - menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(resetToLeft); @@ -134,13 +142,29 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e } else { + if (selectedSingleFolder) + { + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = Directory.Exists(fullPathOfFolder); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(fullPathOfFolder); + ev.Handled = true; + }; + + menu.Items.Add(explore); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + var resetToLeft = new MenuItem(); resetToLeft.Header = App.Text("ChangeCM.ResetFileTo", vm.LeftSideDesc); resetToLeft.Icon = this.CreateMenuIcon("Icons.File.Checkout"); resetToLeft.IsEnabled = vm.CanResetToLeft; resetToLeft.Click += async (_, ev) => { - await vm.ResetMultipleToLeftAsync(selected); + await vm.ResetMultipleToLeftAsync(selection.Changes); ev.Handled = true; }; @@ -150,7 +174,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e resetToRight.IsEnabled = vm.CanResetToRight; resetToRight.Click += async (_, ev) => { - await vm.ResetMultipleToRightAsync(selected); + await vm.ResetMultipleToRightAsync(selection.Changes); ev.Handled = true; }; @@ -160,11 +184,19 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - var builder = new StringBuilder(); - foreach (var c in selected) - builder.AppendLine(c.Path); + if (selectedSingleFolder) + { + await this.CopyTextAsync(relativePathOfFolder); + } + else + { + var builder = new StringBuilder(); + foreach (var c in selection.Changes) + builder.AppendLine(c.Path); + + await this.CopyTextAsync(builder.ToString()); + } - await this.CopyTextAsync(builder.ToString()); ev.Handled = true; }; @@ -174,11 +206,19 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e 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)); + if (selectedSingleFolder) + { + await this.CopyTextAsync(fullPathOfFolder); + } + else + { + var builder = new StringBuilder(); + foreach (var c in selection.Changes) + builder.AppendLine(vm.GetAbsPath(c.Path)); + + await this.CopyTextAsync(builder.ToString()); + } - await this.CopyTextAsync(builder.ToString()); ev.Handled = true; }; @@ -191,7 +231,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e menu.Items.Add(copyFullPath); } - menu.Open(view); + menu.Open(sender as Control); } e.Handled = true; @@ -235,10 +275,7 @@ private async void OnSaveAsPatch(object sender, RoutedEventArgs e) private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) { - if (DataContext is not ViewModels.RevisionCompare vm) - return; - - if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) + if (DataContext is not ViewModels.RevisionCompare { ChangeSelection: { Count: > 0 } selection } vm) return; var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; @@ -246,13 +283,17 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) { var builder = new StringBuilder(); var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - if (selectedChanges.Count == 1) + if (selection.IsSingleFolder) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.SingleFolderPath) : selection.SingleFolderPath); + } + else if (selection.Changes.Count == 1) { - builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.Changes[0].Path) : selection.Changes[0].Path); } else { - foreach (var c in selectedChanges) + foreach (var c in selection.Changes) builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); } diff --git a/src/Views/RevisionCompareStandalone.axaml b/src/Views/RevisionCompareStandalone.axaml new file mode 100644 index 0000000000..42c6778569 --- /dev/null +++ b/src/Views/RevisionCompareStandalone.axaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/RevisionCompareStandalone.axaml.cs b/src/Views/RevisionCompareStandalone.axaml.cs new file mode 100644 index 0000000000..ffd027862b --- /dev/null +++ b/src/Views/RevisionCompareStandalone.axaml.cs @@ -0,0 +1,10 @@ +namespace SourceGit.Views +{ + public partial class RevisionCompareStandalone : ChromelessWindow + { + public RevisionCompareStandalone() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/RevisionFileContentViewer.axaml b/src/Views/RevisionFileContentViewer.axaml index 3422578fe2..f62620f9d1 100644 --- a/src/Views/RevisionFileContentViewer.axaml +++ b/src/Views/RevisionFileContentViewer.axaml @@ -11,11 +11,16 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -96,7 +103,8 @@ + Foreground="{DynamicResource Brush.FG2}" + FontFeatures="+tnum"/> + Width="26" Height="14" + Margin="0,1,0,0" + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=ChangeViewMode, Mode=TwoWay}"/> - @@ -148,17 +155,15 @@ Focusable="False"/> - - - - - - - + + + + + diff --git a/src/Views/StashesPage.axaml.cs b/src/Views/StashesPage.axaml.cs index 2c9af6e9da..678337c457 100644 --- a/src/Views/StashesPage.axaml.cs +++ b/src/Views/StashesPage.axaml.cs @@ -22,10 +22,10 @@ private void OnMainLayoutSizeChanged(object sender, SizeChangedEventArgs e) var layout = ViewModels.Preferences.Instance.Layout; var width = grid.Bounds.Width; - var maxLeft = width - 304; + var leftWidth = Math.Max(width - 304, 300); - if (layout.StashesLeftWidth.Value - maxLeft > 1.0) - layout.StashesLeftWidth = new GridLength(maxLeft, GridUnitType.Pixel); + if (layout.StashesLeftWidth.Value - leftWidth > 1.0) + layout.StashesLeftWidth = new GridLength(leftWidth, GridUnitType.Pixel); } private async void OnStashListKeyDown(object sender, KeyEventArgs e) @@ -141,39 +141,48 @@ private void OnStashDoubleTapped(object sender, TappedEventArgs e) private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.StashesPage { SelectedChanges: { Count: > 0 } selected } vm && - sender is ChangeCollectionView view) + if (DataContext is ViewModels.StashesPage { ChangeSelection: { Count: > 0 } selection } vm) { - var menu = new ContextMenu(); + var selectedSingleFolder = selection.IsSingleFolder; + var fullPathOfFolder = selectedSingleFolder ? vm.GetAbsPath(selection.SingleFolderPath) : null; + var relativePathOfFolder = selectedSingleFolder ? selection.SingleFolderPath : null; - if (selected.Count == 1) + var menu = new ContextMenu(); + if (selection.Count == 1) { - var change = selected[0]; - var fullPath = vm.GetAbsPath(change.Path); - - 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) => + var change = selection.Changes[0]; + var changeFullPath = vm.GetAbsPath(change.Path); + + if (!selection.HasFolder) { - vm.OpenChangeWithExternalDiffTool(change); - ev.Handled = true; - }; - - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = this.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(fullPath); - explore.Click += (_, ev) => + 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.OpenChangeWithExternalDiffTool(change); + ev.Handled = true; + }; + menu.Items.Add(openWithMerger); + } + + if (!selection.HasFolder || selectedSingleFolder) { - Native.OS.OpenInFileManager(fullPath); - ev.Handled = true; - }; + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = selectedSingleFolder ? Directory.Exists(fullPathOfFolder) : File.Exists(changeFullPath); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(selectedSingleFolder ? fullPathOfFolder : changeFullPath); + ev.Handled = true; + }; + menu.Items.Add(explore); + } - menu.Items.Add(openWithMerger); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem { Header = "-" }); + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem { Header = "-" }); } var applyChanges = new MenuItem(); @@ -181,7 +190,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e applyChanges.Icon = this.CreateMenuIcon("Icons.Diff"); applyChanges.Click += async (_, ev) => { - await vm.ApplySelectedChanges(selected); + await vm.ApplySelectedChanges(selection.Changes); ev.Handled = true; }; @@ -190,7 +199,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e checkoutFiles.Icon = this.CreateMenuIcon("Icons.File.Checkout"); checkoutFiles.Click += async (_, ev) => { - await vm.CheckoutFilesAsync(selected); + await vm.CheckoutFilesAsync(selection.Changes); ev.Handled = true; }; @@ -200,14 +209,18 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - if (selected.Count == 1) + if (selectedSingleFolder) + { + await this.CopyTextAsync(relativePathOfFolder); + } + else if (selection.Changes.Count == 1) { - await this.CopyTextAsync(selected[0].Path); + await this.CopyTextAsync(selection.Changes[0].Path); } else { var builder = new StringBuilder(); - foreach (var c in selected) + foreach (var c in selection.Changes) builder.AppendLine(c.Path); await this.CopyTextAsync(builder.ToString()); @@ -222,14 +235,18 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, ev) => { - if (selected.Count == 1) + if (selectedSingleFolder) + { + await this.CopyTextAsync(fullPathOfFolder); + } + else if (selection.Changes.Count == 1) { - await this.CopyTextAsync(vm.GetAbsPath(selected[0].Path)); + await this.CopyTextAsync(vm.GetAbsPath(selection.Changes[0].Path)); } else { var builder = new StringBuilder(); - foreach (var c in selected) + foreach (var c in selection.Changes) builder.AppendLine(vm.GetAbsPath(c.Path)); await this.CopyTextAsync(builder.ToString()); @@ -243,7 +260,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e menu.Items.Add(new MenuItem { Header = "-" }); menu.Items.Add(copyPath); menu.Items.Add(copyFullPath); - menu.Open(view); + menu.Open(sender as Control); } e.Handled = true; @@ -251,23 +268,24 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) { - if (DataContext is not ViewModels.StashesPage vm) - return; - - if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) + if (DataContext is not ViewModels.StashesPage { ChangeSelection: { Count: > 0 } selection } vm) return; if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C) { var builder = new StringBuilder(); var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - if (selectedChanges.Count == 1) + if (selection.IsSingleFolder) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.SingleFolderPath) : selection.SingleFolderPath); + } + else if (selection.Changes.Count == 1) { - builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.Changes[0].Path) : selection.Changes[0].Path); } else { - foreach (var c in selectedChanges) + foreach (var c in selection.Changes) builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); } diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index bc2259976d..48a26774cf 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -5,20 +5,22 @@ xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Statistics" x:DataType="vm:Statistics" x:Name="ThisControl" Title="{DynamicResource Text.Statistics}" - Width="800" Height="500" + Width="1080" Height="720" + MinWidth="1080" MinHeight="720" WindowStartupLocation="CenterOwner" - CanResize="False"> + CanResize="True"> - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + IsVisible="{Binding Total, Mode=OneWay, Converter={x:Static c:IntConverters.IsMaxCountOfStatistics}}" + ToolTip.Tip="{DynamicResource Text.Statistics.Tips}"> + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + - - - - - - - - - + + + + + + + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=ChangeViewMode, Mode=TwoWay}"/> - @@ -168,7 +171,7 @@ Focusable="False"/> - + 0 } selected } vm && - sender is ChangeCollectionView view) + if (DataContext is ViewModels.SubmoduleRevisionCompare { ChangeSelection: { Count: > 0 } selection } vm) { - var menu = new ContextMenu(); - var patch = new MenuItem(); patch.Header = App.Text("FileCM.SaveAsPatch"); patch.Icon = this.CreateMenuIcon("Icons.Save"); @@ -42,7 +39,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e if (storageFile != null) { var saveTo = storageFile.Path.LocalPath; - var succ = await vm.SaveChangesAsPatchAsync(selected, saveTo); + var succ = await vm.SaveChangesAsPatchAsync(selection.Changes, saveTo); if (succ) await new Alert().ShowAsync(this, "Save patch successfully.", false); } @@ -55,37 +52,46 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e e.Handled = true; }; - if (selected.Count == 1) + var selectedSingleFolder = selection.IsSingleFolder; + var fullPathOfFolder = selectedSingleFolder ? vm.GetAbsPath(selection.SingleFolderPath) : null; + var relativePathOfFolder = selectedSingleFolder ? selection.SingleFolderPath : null; + + var menu = new ContextMenu(); + if (selection.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) => + var change = selection.Changes[0]; + var changeFullPath = vm.GetAbsPath(change.Path); + + if (!selection.HasFolder) { - vm.OpenInExternalDiffTool(change); - ev.Handled = true; - }; - menu.Items.Add(openWithMerger); + 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) + if (!selection.HasFolder || selectedSingleFolder) { - 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.IsEnabled = selectedSingleFolder ? Directory.Exists(fullPathOfFolder) : File.Exists(changeFullPath); explore.Click += (_, ev) => { - Native.OS.OpenInFileManager(full); + Native.OS.OpenInFileManager(selectedSingleFolder ? fullPathOfFolder : changeFullPath); ev.Handled = true; }; menu.Items.Add(explore); } - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(patch); + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem() { Header = "-" }); var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); @@ -93,7 +99,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - await this.CopyTextAsync(change.Path); + await this.CopyTextAsync(selectedSingleFolder ? relativePathOfFolder : change.Path); ev.Handled = true; }; @@ -103,16 +109,33 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, ev) => { - await this.CopyTextAsync(vm.GetAbsPath(change.Path)); + await this.CopyTextAsync(selectedSingleFolder ? fullPathOfFolder : changeFullPath); ev.Handled = true; }; + menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copyPath); menu.Items.Add(copyFullPath); } else { + if (selectedSingleFolder) + { + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = Directory.Exists(fullPathOfFolder); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(fullPathOfFolder); + ev.Handled = true; + }; + + menu.Items.Add(explore); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + menu.Items.Add(patch); var copyPath = new MenuItem(); @@ -121,11 +144,19 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, ev) => { - var builder = new StringBuilder(); - foreach (var c in selected) - builder.AppendLine(c.Path); + if (selectedSingleFolder) + { + await this.CopyTextAsync(relativePathOfFolder); + } + else + { + var builder = new StringBuilder(); + foreach (var c in selection.Changes) + builder.AppendLine(c.Path); + + await this.CopyTextAsync(builder.ToString()); + } - await this.CopyTextAsync(builder.ToString()); ev.Handled = true; }; @@ -135,11 +166,19 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e 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)); + if (selectedSingleFolder) + { + await this.CopyTextAsync(fullPathOfFolder); + } + else + { + var builder = new StringBuilder(); + foreach (var c in selection.Changes) + builder.AppendLine(vm.GetAbsPath(c.Path)); + + await this.CopyTextAsync(builder.ToString()); + } - await this.CopyTextAsync(builder.ToString()); ev.Handled = true; }; @@ -148,7 +187,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e menu.Items.Add(copyFullPath); } - menu.Open(view); + menu.Open(sender as Control); } e.Handled = true; @@ -156,10 +195,7 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) { - if (DataContext is not ViewModels.SubmoduleRevisionCompare vm) - return; - - if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) + if (DataContext is not ViewModels.SubmoduleRevisionCompare { ChangeSelection: { Count: > 0 } selection } vm) return; var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; @@ -167,13 +203,17 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) { var builder = new StringBuilder(); var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - if (selectedChanges.Count == 1) + if (selection.IsSingleFolder) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.SingleFolderPath) : selection.SingleFolderPath); + } + else if (selection.Count == 1) { - builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + builder.Append(copyAbsPath ? vm.GetAbsPath(selection.Changes[0].Path) : selection.Changes[0].Path); } else { - foreach (var c in selectedChanges) + foreach (var c in selection.Changes) builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); } diff --git a/src/Views/SubmodulesView.axaml b/src/Views/SubmodulesView.axaml index ce077df5b5..b339b0abc3 100644 --- a/src/Views/SubmodulesView.axaml +++ b/src/Views/SubmodulesView.axaml @@ -48,6 +48,7 @@ - - + @@ -139,7 +139,7 @@ - + @@ -36,7 +36,7 @@ DoubleTapped="OnItemDoubleTapped" ToolTip.Tip="{Binding ToolTip}" ToolTip.Placement="Right"> - + - + @@ -82,22 +82,20 @@ DoubleTapped="OnItemDoubleTapped" ToolTip.Tip="{Binding ToolTip}" ToolTip.Placement="Right"> - + @@ -116,10 +114,7 @@ Data="{StaticResource Icons.Tag}" IsVisible="{Binding IsAnnotated}"/> diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs index 3878fcab22..035a277b13 100644 --- a/src/Views/TagsView.axaml.cs +++ b/src/Views/TagsView.axaml.cs @@ -31,13 +31,16 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) public class TagTreeNodeIcon : UserControl { - public static readonly StyledProperty IsExpandedProperty = - AvaloniaProperty.Register(nameof(IsExpanded)); + public static readonly DirectProperty IsExpandedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsExpanded), + static o => o.IsExpanded, + static (o, v) => o.IsExpanded = v); public bool IsExpanded { - get => GetValue(IsExpandedProperty); - set => SetValue(IsExpandedProperty, value); + get => _isExpanded; + set => SetAndRaise(IsExpandedProperty, ref _isExpanded, value); } protected override void OnDataContextChanged(EventArgs e) @@ -62,38 +65,31 @@ private void UpdateContent() return; } - if (node.Tag != null) - CreateContent(new Thickness(0, 0, 0, 0), "Icons.Tag", node.ToolTip is { IsAnnotated: false }); + if (node.Tag is { } tag) + CreateContent(new Thickness(0, 0, 0, 0), tag.IsAnnotated ? "Icons.Tag" : "Icons.Tag.LinePath"); else if (node.IsExpanded) - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open", false); + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); else - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder", false); + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder"); } - private void CreateContent(Thickness margin, string iconKey, bool stroke) + private void CreateContent(Thickness margin, string iconKey) { if (this.FindResource(iconKey) is not StreamGeometry geo) return; - var path = new Avalonia.Controls.Shapes.Path() + Content = new Avalonia.Controls.Shapes.Path() { Width = 12, Height = 12, - HorizontalAlignment = HorizontalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center, Margin = margin, Data = geo, }; - - if (stroke) - { - path.Fill = Brushes.Transparent; - path.Stroke = this.FindResource("Brush.FG1") as IBrush; - path.StrokeThickness = 1; - } - - Content = path; } + + private bool _isExpanded = false; } public partial class TagsView : UserControl @@ -234,6 +230,7 @@ private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs if (selected.Count == 1) { var tag = selected[0]; + var menu = new ContextMenu(); var createBranch = new MenuItem(); createBranch.Icon = this.CreateMenuIcon("Icons.Branch.Add"); @@ -244,6 +241,22 @@ private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs repo.ShowPopup(new ViewModels.CreateBranch(repo, tag)); ev.Handled = true; }; + menu.Items.Add(createBranch); + + if (repo.CurrentBranch != null && !tag.SHA.Equals(repo.CurrentBranch.Head, StringComparison.Ordinal)) + { + var checkoutCommit = new MenuItem(); + checkoutCommit.Header = App.Text("TagCM.Checkout"); + checkoutCommit.Icon = this.CreateMenuIcon("Icons.Detached"); + checkoutCommit.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CheckoutDetached(repo, tag)); + + e.Handled = true; + }; + menu.Items.Add(checkoutCommit); + } var pushTag = new MenuItem(); pushTag.Header = App.Text("TagCM.Push", tag.Name); @@ -255,6 +268,22 @@ private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs repo.ShowPopup(new ViewModels.PushTag(repo, tag)); ev.Handled = true; }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(pushTag); + + if (repo.CurrentBranch is { IsDetachedHead: false } current) + { + var mergeTag = new MenuItem(); + mergeTag.Header = App.Text("TagCM.Merge", tag.Name, current.Name); + mergeTag.Icon = this.CreateMenuIcon("Icons.Merge"); + mergeTag.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, tag, current.Name)); + ev.Handled = true; + }; + menu.Items.Add(mergeTag); + } var deleteTag = new MenuItem(); deleteTag.Header = App.Text("TagCM.Delete", tag.Name); @@ -265,6 +294,8 @@ private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs repo.ShowPopup(new ViewModels.DeleteTag(repo, tag)); ev.Handled = true; }; + menu.Items.Add(deleteTag); + menu.Items.Add(new MenuItem() { Header = "-" }); var compareWithHead = new MenuItem(); compareWithHead.Header = App.Text("TagCM.CompareWithHead"); @@ -281,6 +312,9 @@ private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs { new ViewModels.CompareCommandPalette(repo, tag).Open(); }; + menu.Items.Add(compareWithHead); + menu.Items.Add(compareWith); + menu.Items.Add(new MenuItem() { Header = "-" }); var archive = new MenuItem(); archive.Icon = this.CreateMenuIcon("Icons.Archive"); @@ -291,16 +325,6 @@ private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs repo.ShowPopup(new ViewModels.Archive(repo, tag)); ev.Handled = true; }; - - var menu = new ContextMenu(); - menu.Items.Add(createBranch); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(pushTag); - menu.Items.Add(deleteTag); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(compareWithHead); - menu.Items.Add(compareWith); - menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(archive); menu.Items.Add(new MenuItem() { Header = "-" }); @@ -440,22 +464,34 @@ private void OnKeyDown(object sender, KeyEventArgs e) { RaiseEvent(new RoutedEventArgs(SearchRequestedEvent)); e.Handled = true; - return; } + else if (e.Key is (Key.Delete or Key.Back) && e.KeyModifiers == KeyModifiers.None) + { + if (DataContext is not ViewModels.Repository repo) + return; - if (e.Key is not (Key.Delete or Key.Back)) - return; - - if (DataContext is not ViewModels.Repository repo) - return; + var selected = (sender as ListBox)?.SelectedItems; + var tags = new List(); + foreach (var item in selected) + { + if (item is ViewModels.TagListItem i) + tags.Add(i.Tag); + else if (item is ViewModels.TagTreeNode n) + CollectTagsInNode(n, tags); + } - var selected = (sender as ListBox)?.SelectedItem; - if (selected is ViewModels.TagTreeNode { Tag: { } tagInNode }) - repo.DeleteTag(tagInNode); - else if (selected is ViewModels.TagListItem { Tag: { } tagInItem }) - repo.DeleteTag(tagInItem); + if (tags.Count == 1) + { + repo.DeleteTag(tags[0]); + } + else if (tags.Count > 1) + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteMultipleTags(repo, tags)); + } - e.Handled = true; + e.Handled = true; + } } private void CollectTagsInNode(ViewModels.TagTreeNode node, List outs) diff --git a/src/Views/TextDiffView.axaml b/src/Views/TextDiffView.axaml index 5b06f4727c..f139727f24 100644 --- a/src/Views/TextDiffView.axaml +++ b/src/Views/TextDiffView.axaml @@ -43,7 +43,15 @@ - + + + + + + + + + - + + - - - + + + + @@ -124,12 +132,11 @@ IsChecked="{Binding IsExpanded, Mode=OneWay}" IsVisible="{Binding !IsRepository}"/> - + + IsVisible="{Binding Status, Converter={x:Static ObjectConverters.IsNotNull}}"> @@ -184,11 +191,11 @@ IsVisible="{Binding IsTrackingStatusVisible}"/> + BorderThickness="1" + BorderBrush="{DynamicResource Brush.FG2}" + Height="20" + CornerRadius="4" + Padding="4,0"> @@ -205,12 +212,14 @@ - + + + diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index 53967f74a5..65538875d6 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -1,10 +1,13 @@ using System; +using System.IO; using System.Threading; + using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { @@ -30,29 +33,45 @@ protected override void OnKeyDown(KeyEventArgs e) { if (SelectedItem is ViewModels.RepositoryNode node && e.KeyModifiers == KeyModifiers.None) { - if (e.Key is Key.Delete or Key.Back) + if (e.Key is Key.Left) { - node.Delete(); + if (node.IsExpanded) + ViewModels.Welcome.Instance.ToggleNodeIsExpanded(node); + else if (FindParent(node) is { } parent) + Select(parent); + e.Handled = true; } - else if (node.IsRepository) - { - if (e.Key == Key.Enter) - { - node.Open(); - e.Handled = true; - } - } - else if ((node.IsExpanded && e.Key == Key.Left) || (!node.IsExpanded && e.Key == Key.Right) || e.Key == Key.Enter) + else if (e.Key is Key.Right && node.SubNodes.Count > 0) { - ViewModels.Welcome.Instance.ToggleNodeIsExpanded(node); - e.Handled = true; + if (!node.IsExpanded) + ViewModels.Welcome.Instance.ToggleNodeIsExpanded(node); + else + Select(node.SubNodes[0]); } } if (!e.Handled) base.OnKeyDown(e); } + + private ViewModels.RepositoryNode FindParent(ViewModels.RepositoryNode 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.RepositoryNode node && node.Depth < item.Depth) + return node; + } + + return null; + } } public partial class Welcome : UserControl @@ -79,7 +98,7 @@ protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); - if (!e.Handled) + if (!e.Handled && e.KeyModifiers == KeyModifiers.None) { if (e.Key == Key.Down && ViewModels.Welcome.Instance.Rows.Count > 0) { @@ -89,34 +108,108 @@ protected override void OnKeyDown(KeyEventArgs e) } else if (e.Key == Key.Escape) { - ViewModels.Welcome.Instance.ClearSearchFilter(); - e.Handled = true; + var page = this.FindAncestorOfType(); + if (page is { DataContext: ViewModels.LauncherPage ctx } && ctx.Popup == null) + OnClearSearchFilter(this, e); + } + else if (e.Key == Key.Enter) + { + if (TreeContainer.SelectedItem is ViewModels.RepositoryNode { IsRepository: true } node) + { + node.Open(); + e.Handled = true; + } + } + else if (e.Key == Key.Delete || e.Key == Key.Back) + { + if (TreeContainer.SelectedItem is ViewModels.RepositoryNode node) + { + node.Delete(); + e.Handled = true; + } } } } + private void OnSearchHotKey(object sender, RoutedEventArgs e) + { + SearchBox.Focus(NavigationMethod.Directional); + e.Handled = true; + } + + private void OnClearSearchFilter(object sender, RoutedEventArgs e) + { + ViewModels.Welcome.Instance.ClearSearchFilter(); + SearchBox.Focus(NavigationMethod.Directional); + e.Handled = true; + } + private void OnTreeNodeContextRequested(object sender, ContextRequestedEventArgs ev) { if (sender is Grid { DataContext: ViewModels.RepositoryNode node } grid) { var menu = new ContextMenu(); - if (!node.IsRepository && node.SubNodes.Count > 0) + var edit = new MenuItem(); + edit.Header = App.Text("Welcome.Edit"); + edit.Icon = this.CreateMenuIcon("Icons.Edit"); + edit.Click += (_, e) => + { + node.Edit(); + e.Handled = true; + }; + + var move = new MenuItem(); + move.Header = App.Text("Welcome.Move"); + move.Icon = this.CreateMenuIcon("Icons.MoveTo"); + move.Click += (_, e) => + { + node.Move(); + e.Handled = true; + }; + + var delete = new MenuItem(); + delete.Header = App.Text("Welcome.Delete"); + delete.Icon = this.CreateMenuIcon("Icons.Clear"); + delete.Click += (_, e) => + { + node.Delete(); + e.Handled = true; + }; + + if (!node.IsRepository) { - var openAll = new MenuItem(); - openAll.Header = App.Text("Welcome.OpenAllInNode"); - openAll.Icon = this.CreateMenuIcon("Icons.Folder.Open"); - openAll.Click += (_, e) => + if (node.SubNodes.Count > 0) { - node.Open(); + var openAll = new MenuItem(); + openAll.Header = App.Text("Welcome.OpenAllInNode"); + openAll.Icon = this.CreateMenuIcon("Icons.Folder.Open"); + openAll.Click += (_, e) => + { + node.Open(); + e.Handled = true; + }; + + menu.Items.Add(openAll); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var addSubFolder = new MenuItem(); + addSubFolder.Header = App.Text("Welcome.AddSubFolder"); + addSubFolder.Icon = this.CreateMenuIcon("Icons.Folder.Add"); + addSubFolder.Click += (_, e) => + { + node.AddSubFolder(); e.Handled = true; }; - menu.Items.Add(openAll); + menu.Items.Add(addSubFolder); + menu.Items.Add(edit); + menu.Items.Add(move); menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(delete); } - - if (node.IsRepository) + else if (Directory.Exists(node.Id)) { var open = new MenuItem(); open.Header = App.Text("Welcome.OpenOrInit"); @@ -150,51 +243,16 @@ private void OnTreeNodeContextRequested(object sender, ContextRequestedEventArgs menu.Items.Add(explore); menu.Items.Add(terminal); menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(edit); + menu.Items.Add(move); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(delete); } else { - var addSubFolder = new MenuItem(); - addSubFolder.Header = App.Text("Welcome.AddSubFolder"); - addSubFolder.Icon = this.CreateMenuIcon("Icons.Folder.Add"); - addSubFolder.Click += (_, e) => - { - node.AddSubFolder(); - e.Handled = true; - }; - menu.Items.Add(addSubFolder); + menu.Items.Add(delete); } - var edit = new MenuItem(); - edit.Header = App.Text("Welcome.Edit"); - edit.Icon = this.CreateMenuIcon("Icons.Edit"); - edit.Click += (_, e) => - { - node.Edit(); - e.Handled = true; - }; - - var move = new MenuItem(); - move.Header = App.Text("Welcome.Move"); - move.Icon = this.CreateMenuIcon("Icons.MoveTo"); - move.Click += (_, e) => - { - node.Move(); - e.Handled = true; - }; - - var delete = new MenuItem(); - delete.Header = App.Text("Welcome.Delete"); - delete.Icon = this.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => - { - node.Delete(); - e.Handled = true; - }; - - menu.Items.Add(edit); - menu.Items.Add(move); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(delete); menu.Open(grid); } @@ -205,29 +263,29 @@ private void OnPointerPressedTreeNode(object sender, PointerPressedEventArgs e) { if (e.GetCurrentPoint(sender as Visual).Properties.IsLeftButtonPressed) { - _pressedTreeNode = true; + _pressTreeNodeEvent = e; _startDragTreeNode = false; - _pressedTreeNodePosition = e.GetPosition(sender as Grid); } else { - _pressedTreeNode = false; + _pressTreeNodeEvent = null; _startDragTreeNode = false; } } private void OnPointerReleasedOnTreeNode(object _1, PointerReleasedEventArgs _2) { - _pressedTreeNode = false; + _pressTreeNodeEvent = null; _startDragTreeNode = false; } private async void OnPointerMovedOverTreeNode(object sender, PointerEventArgs e) { - if (_pressedTreeNode && !_startDragTreeNode && + if (_pressTreeNodeEvent != null && + !_startDragTreeNode && sender is Grid { DataContext: ViewModels.RepositoryNode node } grid) { - var delta = e.GetPosition(grid) - _pressedTreeNodePosition; + var delta = e.GetPosition(grid) - _pressTreeNodeEvent.GetPosition(grid); var sizeSquired = delta.X * delta.X + delta.Y * delta.Y; if (sizeSquired < 64) return; @@ -236,13 +294,13 @@ private async void OnPointerMovedOverTreeNode(object sender, PointerEventArgs e) var data = new DataTransfer(); data.Add(DataTransferItem.Create(_dndRepoNode, node.Id)); - await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move); + await DragDrop.DoDragDropAsync(_pressTreeNodeEvent, data, DragDropEffects.Move); } } private void OnTreeViewLostFocus(object _1, RoutedEventArgs _2) { - _pressedTreeNode = false; + _pressTreeNodeEvent = null; _startDragTreeNode = false; } @@ -277,10 +335,32 @@ private async void DropOnTreeView(object sender, DragEventArgs e) foreach (var item in items) { - var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath); - if (!string.IsNullOrEmpty(path)) + var root = item.Path.LocalPath; + if (!Directory.Exists(root)) + { + if (File.Exists(root)) + root = Path.GetDirectoryName(root); + else + continue; + } + + var rs = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(root); + if (!rs.IsSuccess) + { + if (Models.SafeDirectories.IsUntrustedRepository(rs.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(root, rs.StdErr, out var safeDirectory)) + { + var pageView = this.FindAncestorOfType(); + if (pageView is { DataContext: ViewModels.LauncherPage page }) + { + page.Popup = new ViewModels.TrustRepository(page.Node.Id, root, rs.StdErr, safeDirectory, null, false, false, 0); + break; // Stop processing further because `SourceGit` only supports processing one popup at a time. + } + } + } + else { - await ViewModels.Welcome.Instance.AddRepositoryAsync(path, null, true, false); + await ViewModels.Welcome.Instance.AddRepositoryAsync(rs.StdOut.Trim(), null, true, false); refresh = true; } } @@ -289,7 +369,7 @@ private async void DropOnTreeView(object sender, DragEventArgs e) ViewModels.Welcome.Instance.Refresh(); } - _pressedTreeNode = false; + _pressTreeNodeEvent = null; _startDragTreeNode = false; } @@ -336,10 +416,32 @@ private async void DropOnTreeNode(object sender, DragEventArgs e) foreach (var item in items) { - var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath); - if (!string.IsNullOrEmpty(path)) + var root = item.Path.LocalPath; + if (!Directory.Exists(root)) + { + if (File.Exists(root)) + root = Path.GetDirectoryName(root); + else + continue; + } + + var rs = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(root); + if (!rs.IsSuccess) + { + if (Models.SafeDirectories.IsUntrustedRepository(rs.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(root, rs.StdErr, out var safeDirectory)) + { + var pageView = this.FindAncestorOfType(); + if (pageView is { DataContext: ViewModels.LauncherPage page }) + { + page.Popup = new ViewModels.TrustRepository(page.Node.Id, root, rs.StdErr, safeDirectory, to, false, false, 0); + break; // Stop processing further because `SourceGit` only supports processing one popup at a time. + } + } + } + else { - await ViewModels.Welcome.Instance.AddRepositoryAsync(path, to, true, false); + await ViewModels.Welcome.Instance.AddRepositoryAsync(rs.StdOut.Trim(), to, true, false); refresh = true; } } @@ -348,7 +450,7 @@ private async void DropOnTreeNode(object sender, DragEventArgs e) ViewModels.Welcome.Instance.Refresh(); } - _pressedTreeNode = false; + _pressTreeNodeEvent = null; _startDragTreeNode = false; } @@ -365,8 +467,7 @@ private void OnDoubleTappedTreeNode(object sender, TappedEventArgs e) } } - private bool _pressedTreeNode = false; - private Point _pressedTreeNodePosition = new Point(); + private PointerPressedEventArgs _pressTreeNodeEvent = null; private bool _startDragTreeNode = false; private readonly DataFormat _dndRepoNode = DataFormat.CreateStringApplicationFormat("sourcegit-dnd-repo-node"); private CancellationTokenSource _cancellation = new CancellationTokenSource(); diff --git a/src/Views/WelcomeToolbar.axaml b/src/Views/WelcomeToolbar.axaml index 91f2064fa7..d26613e6cf 100644 --- a/src/Views/WelcomeToolbar.axaml +++ b/src/Views/WelcomeToolbar.axaml @@ -13,7 +13,7 @@ - + @@ -24,13 +24,20 @@ - + - diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index a3bf6fdf75..85a606bd14 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -8,17 +8,17 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" x:Class="SourceGit.Views.WorkingCopy" x:DataType="vm:WorkingCopy"> - + - - + + - + - + @@ -26,46 +26,69 @@ - - - - - - - - - + Margin="6,0,0,0" + Fill="{DynamicResource Brush.FG2}" + Data="{StaticResource Icons.Search}"/> + + + + + + + + + + + - + - + @@ -116,10 +139,6 @@ Click="OnStageAllButtonClicked"> - @@ -128,10 +147,10 @@ x:Name="UnstagedChangesView" IsUnstagedChange="True" Background="{DynamicResource Brush.Contents}" - ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=UnstagedChangeViewMode}" + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=ChangeViewMode}" EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}" Changes="{Binding VisibleUnstaged}" - SelectedChanges="{Binding SelectedUnstaged, Mode=TwoWay}" + Selection="{Binding SelectedUnstaged, Mode=TwoWay}" ContextRequested="OnUnstagedContextRequested" ChangeDoubleTapped="OnUnstagedChangeDoubleTapped" KeyDown="OnUnstagedKeyDown"/> @@ -145,14 +164,14 @@ Focusable="False"/> - + - + - + - @@ -183,10 +198,10 @@ x:Name="StagedChangesView" IsUnstagedChange="False" Background="{DynamicResource Brush.Contents}" - ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=StagedChangeViewMode}" + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=ChangeViewMode}" EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}" Changes="{Binding VisibleStaged}" - SelectedChanges="{Binding SelectedStaged, Mode=TwoWay}" + Selection="{Binding SelectedStaged, Mode=TwoWay}" ContextRequested="OnStagedContextRequested" ChangeDoubleTapped="OnStagedChangeDoubleTapped" KeyDown="OnStagedKeyDown"/> @@ -202,26 +217,24 @@ Focusable="False"/> - + - + - + - - - - - - + + + + @@ -243,10 +256,12 @@ Focusable="False"/> - + - + - + diff --git a/src/Views/WorkingCopy.axaml.cs b/src/Views/WorkingCopy.axaml.cs index 10a0d26309..991808b01d 100644 --- a/src/Views/WorkingCopy.axaml.cs +++ b/src/Views/WorkingCopy.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Avalonia.Controls; using Avalonia.Input; @@ -24,10 +25,10 @@ private void OnMainLayoutSizeChanged(object sender, SizeChangedEventArgs e) var layout = ViewModels.Preferences.Instance.Layout; var width = grid.Bounds.Width; - var maxLeft = width - 304; + var leftWidth = Math.Max(width - 304, 300); - if (layout.WorkingCopyLeftWidth.Value - maxLeft > 1.0) - layout.WorkingCopyLeftWidth = new GridLength(maxLeft, GridUnitType.Pixel); + if (layout.WorkingCopyLeftWidth.Value - leftWidth > 1.0) + layout.WorkingCopyLeftWidth = new GridLength(leftWidth, GridUnitType.Pixel); } private async void OnOpenAssumeUnchanged(object sender, RoutedEventArgs e) @@ -41,40 +42,30 @@ private async void OnOpenAssumeUnchanged(object sender, RoutedEventArgs e) private void OnUnstagedContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm && sender is Control control) + if (DataContext is ViewModels.WorkingCopy { Repository: { } repo, SelectedUnstaged: { Count: > 0 } selection } vm) { - var container = control.FindDescendantOfType(); - var selectedSingleFolder = string.Empty; - if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node }) - selectedSingleFolder = node.FullPath; - - var menu = CreateContextMenuForUnstagedChanges(vm, selectedSingleFolder); - menu?.Open(control); + var menu = CreateContextMenuForUnstagedChanges(repo, vm, selection); + menu?.Open(sender as Control); e.Handled = true; } } private void OnStagedContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm && sender is Control control) + if (DataContext is ViewModels.WorkingCopy { Repository: { } repo, SelectedStaged: { Count: > 0 } selection } vm) { - var container = control.FindDescendantOfType(); - var selectedSingleFolder = string.Empty; - if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node }) - selectedSingleFolder = node.FullPath; - - var menu = CreateContextMenuForStagedChanges(vm, selectedSingleFolder); - menu?.Open(control); + var menu = CreateContextMenuForStagedChanges(repo, vm, selection); + menu?.Open(sender as Control); e.Handled = true; } } private async void OnUnstagedChangeDoubleTapped(object _, RoutedEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm) + if (DataContext is ViewModels.WorkingCopy { SelectedUnstaged: { Count: > 0 } selection } vm) { var next = UnstagedChangesView.GetNextChangeWithoutSelection(); - await vm.StageChangesAsync(vm.SelectedUnstaged, next); + await vm.StageChangesAsync(selection.Changes, next); UnstagedChangesView.TakeFocus(); e.Handled = true; } @@ -82,10 +73,10 @@ private async void OnUnstagedChangeDoubleTapped(object _, RoutedEventArgs e) private async void OnStagedChangeDoubleTapped(object _, RoutedEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm) + if (DataContext is ViewModels.WorkingCopy { SelectedStaged: { Count: > 0 } selection } vm) { var next = StagedChangesView.GetNextChangeWithoutSelection(); - await vm.UnstageChangesAsync(vm.SelectedStaged, next); + await vm.UnstageChangesAsync(selection.Changes, next); StagedChangesView.TakeFocus(); e.Handled = true; } @@ -93,93 +84,128 @@ private async void OnStagedChangeDoubleTapped(object _, RoutedEventArgs e) private async void OnUnstagedKeyDown(object _, KeyEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm) + var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + + if (DataContext is ViewModels.WorkingCopy { SelectedUnstaged: { Count: > 0 } selection } vm) { - var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + var changes = selection.Changes; if (e.Key is Key.Space or Key.Enter) { var next = UnstagedChangesView.GetNextChangeWithoutSelection(); - await vm.StageChangesAsync(vm.SelectedUnstaged, next); + await vm.StageChangesAsync(changes, next); UnstagedChangesView.TakeFocus(); e.Handled = true; } - else if (e.Key is Key.Delete or Key.Back && vm.SelectedUnstaged is { Count: > 0 }) + else if (e.Key is Key.Delete or Key.Back) { - vm.Discard(vm.SelectedUnstaged); + var next = UnstagedChangesView.GetNextChangeWithoutSelection(); + vm.Discard(changes, next); e.Handled = true; } - else if (e.Key is Key.O && e.KeyModifiers == cmdKey && vm.SelectedUnstaged is { Count: 1 }) + else if (e.Key is Key.O && e.KeyModifiers == cmdKey && changes.Count == 1) { - var change = vm.SelectedUnstaged[0]; + var change = changes[0]; var fullpath = Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path); if (File.Exists(fullpath)) Native.OS.OpenWithDefaultEditor(fullpath); e.Handled = true; } - else if (e.Key is Key.C && e.KeyModifiers.HasFlag(cmdKey) && vm.SelectedUnstaged is { Count: 1 }) + else if (e.Key is Key.C && e.KeyModifiers.HasFlag(cmdKey)) { - var change = vm.SelectedUnstaged[0]; - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - await this.CopyTextAsync(Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path)); + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (selection.IsSingleFolder) + { + builder.Append(copyAbsPath ? Native.OS.GetAbsPath(vm.Repository.FullPath, selection.SingleFolderPath) : selection.SingleFolderPath); + } + else if (changes.Count == 1) + { + var change = changes[0]; + builder.Append(copyAbsPath ? Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path) : change.Path); + } else - await this.CopyTextAsync(change.Path); + { + foreach (var c in changes) + builder.AppendLine(copyAbsPath ? Native.OS.GetAbsPath(vm.Repository.FullPath, c.Path) : c.Path); + } - e.Handled = true; - } - else if (e.Key is Key.F && e.KeyModifiers == cmdKey) - { - LocalChangesSearchBox.Focus(); - e.Handled = true; + if (builder.Length > 0) + { + await this.CopyTextAsync(builder.ToString()); + e.Handled = true; + } } } + else if (e.Key is Key.F && e.KeyModifiers == cmdKey) + { + LocalChangesSearchBox.Focus(); + e.Handled = true; + } } private async void OnStagedKeyDown(object _, KeyEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm) + var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + + if (DataContext is ViewModels.WorkingCopy { SelectedStaged: { Count: > 0 } selection } vm) { - var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + var changes = selection.Changes; if (e.Key is Key.Space or Key.Enter) { var next = StagedChangesView.GetNextChangeWithoutSelection(); - await vm.UnstageChangesAsync(vm.SelectedStaged, next); + await vm.UnstageChangesAsync(changes, next); StagedChangesView.TakeFocus(); e.Handled = true; } - else if (e.Key is Key.O && e.KeyModifiers == cmdKey && vm.SelectedStaged is { Count: 1 }) + else if (e.Key is Key.O && e.KeyModifiers == cmdKey && changes.Count == 1) { - var change = vm.SelectedStaged[0]; + var change = changes[0]; var fullpath = Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path); if (File.Exists(fullpath)) Native.OS.OpenWithDefaultEditor(fullpath); e.Handled = true; } - else if (e.Key is Key.C && e.KeyModifiers.HasFlag(cmdKey) && vm.SelectedStaged is { Count: 1 }) + else if (e.Key is Key.C && e.KeyModifiers.HasFlag(cmdKey)) { - var change = vm.SelectedStaged[0]; - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - await this.CopyTextAsync(Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path)); + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (selection.IsSingleFolder) + { + builder.Append(copyAbsPath ? Native.OS.GetAbsPath(vm.Repository.FullPath, selection.SingleFolderPath) : selection.SingleFolderPath); + } + else if (changes.Count == 1) + { + var change = changes[0]; + builder.Append(copyAbsPath ? Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path) : change.Path); + } else - await this.CopyTextAsync(change.Path); + { + foreach (var c in changes) + builder.AppendLine(copyAbsPath ? Native.OS.GetAbsPath(vm.Repository.FullPath, c.Path) : c.Path); + } - e.Handled = true; - } - else if (e.Key is Key.F && e.KeyModifiers == cmdKey) - { - LocalChangesSearchBox.Focus(); - e.Handled = true; + if (builder.Length > 0) + { + await this.CopyTextAsync(builder.ToString()); + e.Handled = true; + } } } + else if (e.Key is Key.F && e.KeyModifiers == cmdKey) + { + LocalChangesSearchBox.Focus(); + e.Handled = true; + } } private async void OnStageSelectedButtonClicked(object _, RoutedEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm) + if (DataContext is ViewModels.WorkingCopy { SelectedUnstaged: { Count: > 0 } selection } vm) { var next = UnstagedChangesView.GetNextChangeWithoutSelection(); - await vm.StageChangesAsync(vm.SelectedUnstaged, next); + await vm.StageChangesAsync(selection.Changes, next); UnstagedChangesView.TakeFocus(); } @@ -196,10 +222,10 @@ private async void OnStageAllButtonClicked(object _, RoutedEventArgs e) private async void OnUnstageSelectedButtonClicked(object _, RoutedEventArgs e) { - if (DataContext is ViewModels.WorkingCopy vm) + if (DataContext is ViewModels.WorkingCopy { SelectedStaged: { Count: > 0 } selection } vm) { var next = StagedChangesView.GetNextChangeWithoutSelection(); - await vm.UnstageChangesAsync(vm.SelectedStaged, next); + await vm.UnstageChangesAsync(selection.Changes, next); StagedChangesView.TakeFocus(); } @@ -263,21 +289,17 @@ private async void OnCommitWithPush(object _, RoutedEventArgs e) e.Handled = true; } - private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy vm, string selectedSingleFolder) + private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.Repository repo, ViewModels.WorkingCopy vm, ViewModels.ChangeSelection selection) { - var repo = vm.Repository; - var selectedUnstaged = vm.SelectedUnstaged; - if (repo == null || selectedUnstaged == null || selectedUnstaged.Count == 0) - return null; - - var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder); + var changes = selection.Changes; var menu = new ContextMenu(); - if (selectedUnstaged.Count == 1) + + if (changes.Count == 1) { - var change = selectedUnstaged[0]; + var change = changes[0]; var path = Native.OS.GetAbsPath(repo.FullPath, change.Path); - if (!change.IsConflicted) + if (!change.IsConflicted && !selection.HasFolder) { TryAddOpenFileToContextMenu(menu, path); @@ -294,18 +316,23 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v menu.Items.Add(diffWithMerger); } - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = this.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Click += (_, e) => + if (!selection.HasFolder || selection.IsSingleFolder) { - var target = hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path; - Native.OS.OpenInFileManager(target); - e.Handled = true; - }; - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); + var absPath = selection.IsSingleFolder ? Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath) : path; + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = Path.Exists(absPath); + explore.Click += (_, e) => + { + Native.OS.OpenInFileManager(absPath); + e.Handled = true; + }; + menu.Items.Add(explore); + } + + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem() { Header = "-" }); if (change.IsConflicted) { @@ -313,7 +340,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v useTheirs.Icon = this.CreateMenuIcon("Icons.Incoming"); useTheirs.Click += async (_, e) => { - await vm.UseTheirsAsync(selectedUnstaged); + await vm.UseTheirsAsync(changes); e.Handled = true; }; @@ -321,7 +348,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v useMine.Icon = this.CreateMenuIcon("Icons.Local"); useMine.Click += async (_, e) => { - await vm.UseMineAsync(selectedUnstaged); + await vm.UseMineAsync(changes); e.Handled = true; }; @@ -360,7 +387,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v mergeBuiltin.Click += async (_, e) => { var head = await new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResultAsync(); - await this.ShowDialogAsync(new ViewModels.MergeConflictEditor(repo, head, change.Path)); + this.ShowWindow(new ViewModels.MergeConflictEditor(repo, head, change.Path)); e.Handled = true; }; @@ -387,7 +414,8 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v stage.Tag = "Enter/Space"; stage.Click += async (_, e) => { - await vm.StageChangesAsync(selectedUnstaged, null); + var next = UnstagedChangesView.GetNextChangeWithoutSelection(); + await vm.StageChangesAsync(changes, next); e.Handled = true; }; @@ -397,7 +425,8 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v discard.Tag = "Back/Delete"; discard.Click += (_, e) => { - vm.Discard(selectedUnstaged); + var next = UnstagedChangesView.GetNextChangeWithoutSelection(); + vm.Discard(changes, next); e.Handled = true; }; @@ -407,7 +436,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v stash.Click += (_, e) => { if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.StashChanges(repo, selectedUnstaged)); + repo.ShowPopup(new ViewModels.StashChanges(repo, changes)); e.Handled = true; }; @@ -430,7 +459,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v { var storageFile = await storageProvider.SaveFilePickerAsync(options); if (storageFile != null) - await vm.SaveChangesToPatchAsync(selectedUnstaged, true, storageFile.Path.LocalPath); + await vm.SaveChangesToPatchAsync(changes, true, storageFile.Path.LocalPath); } catch (Exception exception) { @@ -461,87 +490,86 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v var extension = Path.GetExtension(change.Path); var hasExtra = false; - if (change.WorkTree == Models.ChangeState.Untracked) + if (selection.IsSingleFolder) { var addToIgnore = new MenuItem(); addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore"); addToIgnore.Icon = this.CreateMenuIcon("Icons.GitIgnore"); - if (hasSelectedFolder) - { - var ignoreFolder = new MenuItem(); - ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); - ignoreFolder.Click += (_, e) => - { - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selectedSingleFolder}/")); - e.Handled = true; - }; - addToIgnore.Items.Add(ignoreFolder); - } - else + var ignoreFolder = new MenuItem(); + ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); + ignoreFolder.Click += (_, e) => { - var isRooted = change.Path!.IndexOf('/') <= 0; - var singleFile = new MenuItem(); - singleFile.Header = App.Text("WorkingCopy.AddToGitIgnore.SingleFile"); - singleFile.Click += (_, e) => - { - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.AddToIgnore(repo, change.Path)); - e.Handled = true; - }; - addToIgnore.Items.Add(singleFile); - - if (!string.IsNullOrEmpty(extension)) - { - var byExtension = new MenuItem(); - byExtension.Header = App.Text("WorkingCopy.AddToGitIgnore.Extension", extension); - byExtension.Click += (_, e) => - { - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"*{extension}")); - e.Handled = true; - }; - addToIgnore.Items.Add(byExtension); - - var byExtensionInSameFolder = new MenuItem(); - byExtensionInSameFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.ExtensionInSameFolder", extension); - byExtensionInSameFolder.IsVisible = !isRooted; - byExtensionInSameFolder.Click += (_, e) => - { - var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/'); - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{dir}/*{extension}")); - e.Handled = true; - }; - addToIgnore.Items.Add(byExtensionInSameFolder); - } - } + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selection.SingleFolderPath}/")); + e.Handled = true; + }; + addToIgnore.Items.Add(ignoreFolder); menu.Items.Add(addToIgnore); hasExtra = true; } - else if (hasSelectedFolder) + else if (!selection.HasFolder && change.WorkTree == Models.ChangeState.Untracked) { var addToIgnore = new MenuItem(); addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore"); addToIgnore.Icon = this.CreateMenuIcon("Icons.GitIgnore"); - var ignoreFolder = new MenuItem(); - ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); - ignoreFolder.Click += (_, e) => + var isRooted = change.Path!.IndexOf('/') <= 0; + var singleFile = new MenuItem(); + singleFile.Header = App.Text("WorkingCopy.AddToGitIgnore.SingleFile"); + singleFile.Click += (_, e) => { if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selectedSingleFolder}/")); + repo.ShowPopup(new ViewModels.AddToIgnore(repo, change.Path)); e.Handled = true; }; - addToIgnore.Items.Add(ignoreFolder); + addToIgnore.Items.Add(singleFile); + + if (!string.IsNullOrEmpty(extension)) + { + var byExtension = new MenuItem(); + byExtension.Header = App.Text("WorkingCopy.AddToGitIgnore.Extension", extension); + byExtension.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"*{extension}")); + e.Handled = true; + }; + addToIgnore.Items.Add(byExtension); + + var byExtensionInSameFolder = new MenuItem(); + byExtensionInSameFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.ExtensionInSameFolder", extension); + byExtensionInSameFolder.IsVisible = !isRooted; + byExtensionInSameFolder.Click += (_, e) => + { + var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/'); + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{dir}/*{extension}")); + e.Handled = true; + }; + addToIgnore.Items.Add(byExtensionInSameFolder); + } + + if (!isRooted) + { + var untrackedInSameFolder = new MenuItem(); + untrackedInSameFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.UntrackedInSameFolder"); + untrackedInSameFolder.Click += (_, e) => + { + var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/'); + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{dir}/")); + e.Handled = true; + }; + addToIgnore.Items.Add(untrackedInSameFolder); + } menu.Items.Add(addToIgnore); hasExtra = true; } - if (File.Exists(path) && repo.IsLFSEnabled()) + if (!selection.HasFolder && File.Exists(path) && repo.IsLFSEnabled()) { var lfs = new MenuItem(); lfs.Header = App.Text("GitLFS"); @@ -641,14 +669,14 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v menu.Items.Add(new MenuItem() { Header = "-" }); } - if (hasSelectedFolder) + if (selection.IsSingleFolder) { var history = new MenuItem(); history.Header = App.Text("DirHistories"); history.Icon = this.CreateMenuIcon("Icons.Histories"); history.Click += (_, e) => { - this.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder)); + this.ShowWindow(new ViewModels.DirHistories(repo, selection.SingleFolderPath)); e.Handled = true; }; @@ -689,7 +717,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copy.Click += async (_, e) => { - await this.CopyTextAsync(hasSelectedFolder ? selectedSingleFolder : change.Path); + await this.CopyTextAsync(selection.IsSingleFolder ? selection.SingleFolderPath : change.Path); e.Handled = true; }; @@ -699,7 +727,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, e) => { - await this.CopyTextAsync(hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path); + await this.CopyTextAsync(selection.IsSingleFolder ? Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath) : path); e.Handled = true; }; @@ -710,7 +738,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v { var hasConflicts = false; var hasNonConflicts = false; - foreach (var change in selectedUnstaged) + foreach (var change in changes) { if (change.IsConflicted) hasConflicts = true; @@ -730,7 +758,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v useTheirs.Icon = this.CreateMenuIcon("Icons.Incoming"); useTheirs.Click += async (_, e) => { - await vm.UseTheirsAsync(selectedUnstaged); + await vm.UseTheirsAsync(changes); e.Handled = true; }; @@ -738,7 +766,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v useMine.Icon = this.CreateMenuIcon("Icons.Local"); useMine.Click += async (_, e) => { - await vm.UseMineAsync(selectedUnstaged); + await vm.UseMineAsync(changes); e.Handled = true; }; @@ -771,9 +799,9 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v return menu; } - if (hasSelectedFolder) + if (selection.IsSingleFolder) { - var dir = Path.Combine(repo.FullPath, selectedSingleFolder); + var dir = Path.Combine(repo.FullPath, selection.SingleFolderPath); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = this.CreateMenuIcon("Icons.Explore"); @@ -788,32 +816,34 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v } var stage = new MenuItem(); - stage.Header = App.Text("FileCM.StageMulti", selectedUnstaged.Count); + stage.Header = App.Text("FileCM.StageMulti", changes.Count); stage.Icon = this.CreateMenuIcon("Icons.File.Add"); stage.Tag = "Enter/Space"; stage.Click += async (_, e) => { - await vm.StageChangesAsync(selectedUnstaged, null); + var next = UnstagedChangesView.GetNextChangeWithoutSelection(); + await vm.StageChangesAsync(changes, next); e.Handled = true; }; var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", selectedUnstaged.Count); + discard.Header = App.Text("FileCM.DiscardMulti", changes.Count); discard.Icon = this.CreateMenuIcon("Icons.Undo"); discard.Tag = "Back/Delete"; discard.Click += (_, e) => { - vm.Discard(selectedUnstaged); + var next = UnstagedChangesView.GetNextChangeWithoutSelection(); + vm.Discard(changes, next); e.Handled = true; }; var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", selectedUnstaged.Count); + stash.Header = App.Text("FileCM.StashMulti", changes.Count); stash.Icon = this.CreateMenuIcon("Icons.Stashes.Add"); stash.Click += (_, e) => { if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.StashChanges(repo, selectedUnstaged)); + repo.ShowPopup(new ViewModels.StashChanges(repo, changes)); e.Handled = true; }; @@ -836,7 +866,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v { var storageFile = await storageProvider.SaveFilePickerAsync(options); if (storageFile != null) - await vm.SaveChangesToPatchAsync(selectedUnstaged, true, storageFile.Path.LocalPath); + await vm.SaveChangesToPatchAsync(changes, true, storageFile.Path.LocalPath); } catch (Exception exception) { @@ -851,14 +881,14 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v menu.Items.Add(stash); menu.Items.Add(patch); - if (hasSelectedFolder) + if (selection.IsSingleFolder) { var ignoreFolder = new MenuItem(); ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); ignoreFolder.Click += (_, e) => { if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selectedSingleFolder}/")); + repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selection.SingleFolderPath}/")); e.Handled = true; }; @@ -872,7 +902,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v history.Icon = this.CreateMenuIcon("Icons.Histories"); history.Click += (_, e) => { - this.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder)); + this.ShowWindow(new ViewModels.DirHistories(repo, selection.SingleFolderPath)); e.Handled = true; }; @@ -882,7 +912,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copy.Click += async (_, e) => { - await this.CopyTextAsync(selectedSingleFolder); + await this.CopyTextAsync(selection.SingleFolderPath); e.Handled = true; }; @@ -892,7 +922,7 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, e) => { - await this.CopyTextAsync(Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder)); + await this.CopyTextAsync(Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath)); e.Handled = true; }; @@ -909,13 +939,9 @@ private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy v return menu; } - public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, string selectedSingleFolder) + public ContextMenu CreateContextMenuForStagedChanges(ViewModels.Repository repo, ViewModels.WorkingCopy vm, ViewModels.ChangeSelection selection) { - var repo = vm.Repository; - var selectedStaged = vm.SelectedStaged; - if (repo == null || selectedStaged == null || selectedStaged.Count == 0) - return null; - + var changes = selection.Changes; var menu = new ContextMenu(); MenuItem ai = null; @@ -930,7 +956,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, { ai.Click += (_, e) => { - DoOpenAIAssistant(repo, services[0], selectedStaged); + DoOpenAIAssistant(repo, services[0], changes); e.Handled = true; }; } @@ -944,7 +970,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, item.Header = service.Name; item.Click += (_, e) => { - DoOpenAIAssistant(repo, dup, selectedStaged); + DoOpenAIAssistant(repo, dup, changes); e.Handled = true; }; @@ -953,32 +979,45 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, } } - var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder); - if (selectedStaged.Count == 1) + if (changes.Count == 1) { - var change = selectedStaged[0]; + var change = changes[0]; var path = Native.OS.GetAbsPath(repo.FullPath, change.Path); - 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) => + if (!selection.HasFolder) { - vm.UseExternalDiffTool(change, false); - ev.Handled = true; - }; + TryAddOpenFileToContextMenu(menu, path); + + 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.UseExternalDiffTool(change, false); + ev.Handled = true; + }; + menu.Items.Add(openWithMerger); + } - var explore = new MenuItem(); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Header = App.Text("RevealFile"); - explore.Icon = this.CreateMenuIcon("Icons.Explore"); - explore.Click += (_, e) => + if (!selection.HasFolder || selection.IsSingleFolder) { - var target = hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path; - Native.OS.OpenInFileManager(target); - e.Handled = true; - }; + var absPath = selection.IsSingleFolder ? Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath) : path; + var explore = new MenuItem(); + explore.IsEnabled = File.Exists(path) || Directory.Exists(path); + explore.Header = App.Text("RevealFile"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = Path.Exists(absPath); + explore.Click += (_, e) => + { + Native.OS.OpenInFileManager(absPath); + e.Handled = true; + }; + menu.Items.Add(explore); + } + + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem() { Header = "-" }); var unstage = new MenuItem(); unstage.Header = App.Text("FileCM.Unstage"); @@ -986,17 +1025,19 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, unstage.Tag = "Enter/Space"; unstage.Click += async (_, e) => { - await vm.UnstageChangesAsync(selectedStaged, null); + var next = StagedChangesView.GetNextChangeWithoutSelection(); + await vm.UnstageChangesAsync(changes, next); e.Handled = true; }; var stash = new MenuItem(); stash.Header = App.Text("FileCM.Stash"); stash.Icon = this.CreateMenuIcon("Icons.Stashes.Add"); + stash.IsEnabled = !vm.UseAmend; stash.Click += (_, e) => { if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.StashChanges(repo, selectedStaged)); + repo.ShowPopup(new ViewModels.StashChanges(repo, changes)); e.Handled = true; }; @@ -1019,7 +1060,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, { var storageFile = await storageProvider.SaveFilePickerAsync(options); if (storageFile != null) - await vm.SaveChangesToPatchAsync(selectedStaged, false, storageFile.Path.LocalPath); + await vm.SaveChangesToPatchAsync(changes, false, storageFile.Path.LocalPath); } catch (Exception exception) { @@ -1029,16 +1070,12 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, e.Handled = true; }; - TryAddOpenFileToContextMenu(menu, path); - menu.Items.Add(openWithMerger); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(unstage); menu.Items.Add(stash); menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); - if (File.Exists(path) && repo.IsLFSEnabled()) + if (!selection.HasFolder && File.Exists(path) && repo.IsLFSEnabled()) { var lfs = new MenuItem(); lfs.Header = App.Text("GitLFS"); @@ -1112,14 +1149,14 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, menu.Items.Add(new MenuItem() { Header = "-" }); } - if (hasSelectedFolder) + if (selection.IsSingleFolder) { var history = new MenuItem(); history.Header = App.Text("DirHistories"); history.Icon = this.CreateMenuIcon("Icons.Histories"); history.Click += (_, e) => { - this.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder)); + this.ShowWindow(new ViewModels.DirHistories(repo, selection.SingleFolderPath)); e.Handled = true; }; @@ -1160,7 +1197,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, e) => { - await this.CopyTextAsync(hasSelectedFolder ? selectedSingleFolder : change.Path); + await this.CopyTextAsync(selection.IsSingleFolder ? selection.SingleFolderPath : change.Path); e.Handled = true; }; @@ -1170,7 +1207,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, e) => { - var target = hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path; + var target = selection.IsSingleFolder ? Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath) : path; await this.CopyTextAsync(target); e.Handled = true; }; @@ -1180,9 +1217,9 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, } else { - if (hasSelectedFolder) + if (selection.IsSingleFolder) { - var dir = Path.Combine(repo.FullPath, selectedSingleFolder); + var dir = Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath); var explore = new MenuItem(); explore.IsEnabled = Directory.Exists(dir); explore.Header = App.Text("RevealFile"); @@ -1198,22 +1235,24 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, } var unstage = new MenuItem(); - unstage.Header = App.Text("FileCM.UnstageMulti", selectedStaged.Count); + unstage.Header = App.Text("FileCM.UnstageMulti", changes.Count); unstage.Icon = this.CreateMenuIcon("Icons.File.Remove"); unstage.Tag = "Enter/Space"; unstage.Click += async (_, e) => { - await vm.UnstageChangesAsync(selectedStaged, null); + var next = StagedChangesView.GetNextChangeWithoutSelection(); + await vm.UnstageChangesAsync(changes, next); e.Handled = true; }; var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", selectedStaged.Count); + stash.Header = App.Text("FileCM.StashMulti", changes.Count); stash.Icon = this.CreateMenuIcon("Icons.Stashes.Add"); + stash.IsEnabled = !vm.UseAmend; stash.Click += (_, e) => { if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.StashChanges(repo, selectedStaged)); + repo.ShowPopup(new ViewModels.StashChanges(repo, changes)); e.Handled = true; }; @@ -1236,7 +1275,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, { var storageFile = await storageProvider.SaveFilePickerAsync(options); if (storageFile != null) - await vm.SaveChangesToPatchAsync(selectedStaged, false, storageFile.Path.LocalPath); + await vm.SaveChangesToPatchAsync(changes, false, storageFile.Path.LocalPath); } catch (Exception exception) { @@ -1256,14 +1295,14 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, menu.Items.Add(ai); } - if (hasSelectedFolder) + if (selection.IsSingleFolder) { var history = new MenuItem(); history.Header = App.Text("DirHistories"); history.Icon = this.CreateMenuIcon("Icons.Histories"); history.Click += (_, e) => { - this.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder)); + this.ShowWindow(new ViewModels.DirHistories(repo, selection.SingleFolderPath)); e.Handled = true; }; @@ -1273,7 +1312,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; copyPath.Click += async (_, e) => { - await this.CopyTextAsync(selectedSingleFolder); + await this.CopyTextAsync(selection.SingleFolderPath); e.Handled = true; }; @@ -1283,7 +1322,7 @@ public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; copyFullPath.Click += async (_, e) => { - await this.CopyTextAsync(Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder)); + await this.CopyTextAsync(Native.OS.GetAbsPath(repo.FullPath, selection.SingleFolderPath)); e.Handled = true; }; diff --git a/tools/setsid-macos/setsid.c b/tools/setsid-macos/setsid.c new file mode 100644 index 0000000000..15b697eac1 --- /dev/null +++ b/tools/setsid-macos/setsid.c @@ -0,0 +1,16 @@ +#include +#include +#include + +int main(int argc, char* argv[]) { + if (argc < 2) { + err(EXIT_FAILURE, "Usage: setsid [arguments ...]"); + } + + if (setsid() < 0) { + err(EXIT_FAILURE, "setsid failed"); + } + + execvp(argv[1], argv + 1); + err(EXIT_FAILURE, "Failed to execute %s", argv[1]); +}