diff --git a/.editorconfig b/.editorconfig index 22c741b9..f3c9a7bf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -71,20 +71,20 @@ dotnet_style_predefined_type_for_member_access = true:suggestion # name all constant fields using PascalCase dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case # private static fields should have s_ prefix dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style -dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_kinds = field dotnet_naming_symbols.private_static_fields.required_modifiers = static dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private @@ -93,14 +93,14 @@ dotnet_naming_style.private_static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case # use accessibility modifiers dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bcb32580..12792cf6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,7 +58,7 @@ jobs: if: ${{ matrix.runtime == 'linux-arm64' }} run: | sudo apt-get update - sudo apt-get install -y llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64 + sudo apt-get install -y llvm gcc-aarch64-linux-gnu - name: Build run: dotnet build -c Release - name: Publish diff --git a/.github/workflows/localization-check.yml b/.github/workflows/localization-check.yml index cc5201ab..8dcd61c8 100644 --- a/.github/workflows/localization-check.yml +++ b/.github/workflows/localization-check.yml @@ -4,7 +4,6 @@ on: branches: [ develop ] paths: - 'src/Resources/Locales/**' - - 'README.md' workflow_dispatch: workflow_call: @@ -32,8 +31,8 @@ jobs: git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' if [ -n "$(git status --porcelain)" ]; then - git add README.md TRANSLATION.md - git commit -m 'doc: Update translation status and missing keys' + git add TRANSLATION.md src/Resources/Locales/*.axaml + git commit -m 'doc: Update translation status and sort locale files' git push else echo "No changes to commit" diff --git a/.gitignore b/.gitignore index 0c66b11e..e686a534 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ build/*.deb build/*.rpm build/*.AppImage SourceGit.app/ +build.command +src/Properties/launchSettings.json diff --git a/README.md b/README.md index 846407f6..f9ba3072 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * Supports Windows/macOS/Linux * Opensource/Free * Fast -* Deutsch/English/Español/Français/Italiano/Português/Русский/简体中文/繁體中文/日本語/தமிழ் (Tamil) +* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil) * Built-in light/dark themes * Customize theme * Visual commit graph @@ -35,9 +35,11 @@ * Revision Diffs * Branch Diff * Image Diff - Side-By-Side/Swipe/Blend +* Git command logs * Search commits * GitFlow * Git LFS +* Bisect * Issue Link * Workspace * Custom Action @@ -52,9 +54,9 @@ You can find the current translation status in [TRANSLATION.md](https://github.c ## How to Use -**To use this tool, you need to install Git(>=2.23.0) first.** +**To use this tool, you need to install Git(>=2.25.1) first.** -You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [Github Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits. +You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits. This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs. @@ -91,7 +93,7 @@ For **macOS** users: brew tap ybeapps/homebrew-sourcegit brew install --cask --no-quarantine sourcegit ``` -* If you want to install `SourceGit.app` from Github Release manually, you need run following command to make sure it works: +* If you want to install `SourceGit.app` from GitHub Release manually, you need run following command to make sure it works: ```shell sudo xattr -cr /Applications/SourceGit.app ``` diff --git a/TRANSLATION.md b/TRANSLATION.md index a2991c6e..1fa3259d 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,111 +6,220 @@ 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-97.34%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-%E2%88%9A-brightgreen) -
-Missing keys in de_DE.axaml - -- Text.BranchUpstreamInvalid -- Text.Configure.CustomAction.WaitForExit -- Text.Configure.Git.PreferredMergeMode -- Text.Configure.IssueTracker.AddSampleAzure -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.CopyFullPath -- Text.Diff.First -- Text.Diff.Last -- Text.Preferences.AI.Streaming -- Text.Preferences.Appearance.EditorTabWidth -- Text.Preferences.General.ShowTagsInGraph -- Text.StashCM.SaveAsPatch -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs - -
- -### ![es__ES](https://img.shields.io/badge/es__ES-98.67%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.13%25-yellow)
Missing keys in es_ES.axaml -- Text.Configure.Git.PreferredMergeMode -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs +- Text.CommitCM.PushRevision +- Text.Merge.Edit +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Stash.Mode +- Text.StashCM.CopyMessage +- Text.WorkingCopy.AddToGitIgnore.InFolder
-### ![fr__FR](https://img.shields.io/badge/fr__FR-98.67%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-91.19%25-yellow)
Missing keys in fr_FR.axaml +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitCM.PushRevision +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount - Text.Configure.Git.PreferredMergeMode - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Edit +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Stash.Mode +- Text.StashCM.CopyMessage +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.AddToGitIgnore.InFolder - Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor
-### ![it__IT](https://img.shields.io/badge/it__IT-98.40%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-96.53%25-yellow)
Missing keys in it_IT.axaml -- Text.Configure.Git.PreferredMergeMode -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.CopyFullPath -- Text.Preferences.General.ShowTagsInGraph -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs +- Text.Avatar.Load +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.PushRevision +- Text.CommitDetail.Changes.Count +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Edit +- Text.Pull.RecurseSubmodules +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Repository.ClearStashes +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Stash.Mode +- Text.StashCM.CopyMessage +- Text.Submodule.Deinit +- Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.ResetAuthor
-### ![ja__JP](https://img.shields.io/badge/ja__JP-98.40%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-90.94%25-yellow)
Missing keys in ja_JP.axaml +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.CompareWithCurrent +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitCM.PushRevision +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount - Text.Configure.Git.PreferredMergeMode - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Edit +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes - Text.Repository.FilterCommits -- Text.Repository.Tags.OrderByNameDes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Stash.Mode +- Text.StashCM.CopyMessage +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.AddToGitIgnore.InFolder - Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor
-### ![pt__BR](https://img.shields.io/badge/pt__BR-89.76%25-yellow) +### ![pt__BR](https://img.shields.io/badge/pt__BR-83.25%25-yellow)
Missing keys in pt_BR.axaml @@ -121,14 +230,32 @@ This document shows the translation status of each locale file in the repository - Text.ApplyStash.DropAfterApply - Text.ApplyStash.RestoreIndex - Text.ApplyStash.Stash +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange - Text.BranchCM.CustomAction - Text.BranchCM.MergeMultiBranches +- Text.BranchCM.ResetToSelectedCommit - Text.BranchUpstreamInvalid +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream - Text.Clone.RecurseSubmodules +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject - Text.CommitCM.Merge - Text.CommitCM.MergeMultiple +- Text.CommitCM.PushRevision +- Text.CommitDetail.Changes.Count - Text.CommitDetail.Files.Search - Text.CommitDetail.Info.Children +- Text.CommitMessageTextBox.SubjectCount - Text.Configure.CustomAction.Scope.Branch - Text.Configure.CustomAction.WaitForExit - Text.Configure.Git.PreferredMergeMode @@ -140,19 +267,32 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.WithLocalChanges - Text.CopyFullPath - Text.CreateBranch.Name.WarnSpace +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path - Text.DeleteRepositoryNode.Path - Text.DeleteRepositoryNode.TipForGroup - Text.DeleteRepositoryNode.TipForRepository - Text.Diff.First - Text.Diff.Last +- Text.Diff.Submodule.Deleted - Text.Diff.UseBlockNavigation - Text.Fetch.Force - Text.FileCM.ResolveUsing +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Hotkeys.Global.Clone +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool - Text.InProgress.CherryPick.Head - Text.InProgress.Merge.Operating - Text.InProgress.Rebase.StoppedAt - Text.InProgress.Revert.Head +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Edit - Text.Merge.Source - Text.MergeMultiple - Text.MergeMultiple.CommitChanges @@ -163,7 +303,15 @@ This document shows the translation status of each locale file in the repository - Text.Preferences.General.DateFormat - Text.Preferences.General.ShowChildren - Text.Preferences.General.ShowTagsInGraph +- Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.SSLVerify +- Text.Pull.RecurseSubmodules +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes - Text.Repository.FilterCommits - Text.Repository.HistoriesLayout - Text.Repository.HistoriesLayout.Horizontal @@ -171,47 +319,198 @@ This document shows the translation status of each locale file in the repository - Text.Repository.HistoriesOrder - Text.Repository.Notifications.Clear - Text.Repository.OnlyHighlightCurrentBranchInHistories +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree - Text.Repository.Skip - Text.Repository.Tags.OrderByCreatorDate -- Text.Repository.Tags.OrderByNameAsc -- Text.Repository.Tags.OrderByNameDes +- Text.Repository.Tags.OrderByName - Text.Repository.Tags.Sort - Text.Repository.UseRelativeTimeInHistories +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target - Text.SetUpstream - Text.SetUpstream.Local - Text.SetUpstream.Unset - Text.SetUpstream.Upstream - Text.SHALinkCM.NavigateTo -- Text.Stash.AutoRestore -- Text.Stash.AutoRestore.Tip +- Text.Stash.Mode +- Text.StashCM.CopyMessage - Text.StashCM.SaveAsPatch +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.AddToGitIgnore.InFolder - Text.WorkingCopy.CommitToEdit - Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor - Text.WorkingCopy.SignOff
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) -### ![ta__IN](https://img.shields.io/badge/ta__IN-98.67%25-yellow) +### ![ta__IN](https://img.shields.io/badge/ta__IN-91.07%25-yellow)
Missing keys in ta_IN.axaml +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.CompareWithCurrent +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitCM.PushRevision +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount - Text.Configure.Git.PreferredMergeMode - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Edit +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Stash.Mode +- Text.StashCM.CopyMessage +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL - Text.UpdateSubmodules.Target +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.AddToGitIgnore.InFolder - Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor + +
+ +### ![uk__UA](https://img.shields.io/badge/uk__UA-92.31%25-yellow) + +
+Missing keys in uk_UA.axaml + +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitCM.PushRevision +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount +- Text.ConfigureWorkspace.Name +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Edit +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Push.Revision +- Text.Push.Revision.Title +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Stash.Mode +- Text.StashCM.CopyMessage +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.ResetAuthor
diff --git a/VERSION b/VERSION index c29bf2a5..11692c6b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.13 \ No newline at end of file +2025.23 \ No newline at end of file diff --git a/build/README.md b/build/README.md index b4358a55..17305edf 100644 --- a/build/README.md +++ b/build/README.md @@ -12,4 +12,4 @@ dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj ``` > [!NOTE] -> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replece the `$DESTINATION_FOLDER` with the real path that will store the output executable files. +> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replace the `$DESTINATION_FOLDER` with the real path that will store the output executable files. diff --git a/build/scripts/localization-check.js b/build/scripts/localization-check.js index 1e8f1f0d..8d636b5b 100644 --- a/build/scripts/localization-check.js +++ b/build/scripts/localization-check.js @@ -14,6 +14,22 @@ async function parseXml(filePath) { return parser.parseStringPromise(data); } +async function filterAndSortTranslations(localeData, enUSKeys, enUSData) { + const strings = localeData.ResourceDictionary['x:String']; + // Remove keys that don't exist in English file + const filtered = strings.filter(item => enUSKeys.has(item.$['x:Key'])); + + // Sort based on the key order in English file + const enUSKeysArray = enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']); + filtered.sort((a, b) => { + const aIndex = enUSKeysArray.indexOf(a.$['x:Key']); + const bIndex = enUSKeysArray.indexOf(b.$['x:Key']); + return aIndex - bIndex; + }); + + return filtered; +} + async function calculateTranslationRate() { const enUSData = await parseXml(enUSFile); const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key'])); @@ -33,6 +49,21 @@ async function calculateTranslationRate() { const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key'])); const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key)); + // Sort and clean up extra translations + const sortedAndCleaned = await filterAndSortTranslations(localeData, enUSKeys, enUSData); + localeData.ResourceDictionary['x:String'] = sortedAndCleaned; + + // Save the updated file + const builder = new xml2js.Builder({ + headless: true, + renderOpts: { pretty: true, indent: ' ' } + }); + let xmlStr = builder.buildObject(localeData); + + // Add an empty line before the first x:String + xmlStr = xmlStr.replace(' 0) { const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100; const badgeColor = progress >= 75 ? 'yellow' : 'red'; @@ -41,7 +72,7 @@ async function calculateTranslationRate() { lines.push(`
\nMissing keys in ${file}\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n
`) } else { lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)`); - } + } } const content = lines.join('\n\n'); diff --git a/src/App.Commands.cs b/src/App.Commands.cs index 85a75829..22e9fb51 100644 --- a/src/App.Commands.cs +++ b/src/App.Commands.cs @@ -37,10 +37,10 @@ namespace SourceGit } } - public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences())); - public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys())); + public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false)); + public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false)); public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir)); - public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About())); + public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false)); public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true)); public static readonly Command QuitCommand = new Command(_ => Quit(0)); public static readonly Command CopyTextBlockCommand = new Command(p => diff --git a/src/App.axaml b/src/App.axaml index 9d8adb89..186022d5 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -16,6 +16,7 @@ + @@ -34,7 +35,7 @@ - + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 0448a247..6b42700d 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -77,7 +78,7 @@ namespace SourceGit return builder; } - private static void LogException(Exception ex) + public static void LogException(Exception ex) { if (ex == null) return; @@ -104,10 +105,44 @@ namespace SourceGit #endregion #region Utility Functions - public static void OpenDialog(Window window) + public static void ShowWindow(object data, bool showAsDialog) { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) - window.ShowDialog(owner); + var impl = (Views.ChromelessWindow target, bool isDialog) => + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + if (isDialog) + target.ShowDialog(owner); + else + target.Show(owner); + } + else + { + target.Show(); + } + }; + + if (data is Views.ChromelessWindow window) + { + impl(window, showAsDialog); + return; + } + + var dataTypeName = data.GetType().FullName; + if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal)) + return; + + var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views."); + var viewType = Type.GetType(viewTypeName); + if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow))) + return; + + window = Activator.CreateInstance(viewType) as Views.ChromelessWindow; + if (window != null) + { + window.DataContext = data; + impl(window, showAsDialog); + } } public static void RaiseException(string context, string message) @@ -266,7 +301,7 @@ namespace SourceGit return await clipboard.GetTextAsync(); } } - return default; + return null; } public static string Text(string key, params object[] args) @@ -288,8 +323,7 @@ namespace SourceGit icon.Height = 12; icon.Stretch = Stretch.Uniform; - var geo = Current?.FindResource(key) as StreamGeometry; - if (geo != null) + if (Current?.FindResource(key) is StreamGeometry geo) icon.Data = geo; return icon; @@ -303,7 +337,7 @@ namespace SourceGit return null; } - public static ViewModels.Launcher GetLauncer() + public static ViewModels.Launcher GetLauncher() { return Current is App app ? app._launcher : null; } @@ -341,13 +375,42 @@ namespace SourceGit { BindingPlugins.DataValidators.RemoveAt(0); + // Disable tooltip if window is not active. + ToolTip.ToolTipOpeningEvent.AddClassHandler((c, e) => + { + var topLevel = TopLevel.GetTopLevel(c); + if (topLevel is not Window { IsActive: true }) + e.Cancel = true; + }); + if (TryLaunchAsCoreEditor(desktop)) return; if (TryLaunchAsAskpass(desktop)) return; - TryLaunchAsNormal(desktop); + _ipcChannel = new Models.IpcChannel(); + if (!_ipcChannel.IsFirstInstance) + { + var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty; + if (!string.IsNullOrEmpty(arg)) + { + if (arg.StartsWith('"') && arg.EndsWith('"')) + arg = arg.Substring(1, arg.Length - 2).Trim(); + + if (arg.Length > 0 && !Path.IsPathFullyQualified(arg)) + arg = Path.GetFullPath(arg); + } + + _ipcChannel.SendToFirstInstance(arg); + Environment.Exit(0); + } + else + { + _ipcChannel.MessageReceived += TryOpenRepository; + desktop.Exit += (_, _) => _ipcChannel.Dispose(); + TryLaunchAsNormal(desktop); + } } } #endregion @@ -420,21 +483,37 @@ namespace SourceGit return true; var gitDir = Path.GetDirectoryName(file)!; - var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) - return true; - - var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); + var origHeadFile = Path.Combine(gitDir, "rebase-merge", "orig-head"); + var ontoFile = Path.Combine(gitDir, "rebase-merge", "onto"); var doneFile = Path.Combine(gitDir, "rebase-merge", "done"); - if (!File.Exists(doneFile)) + var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json"); + if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile)) return true; - var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); - if (done.Length > collection.Jobs.Count) + var origHead = File.ReadAllText(origHeadFile).Trim(); + var onto = File.ReadAllText(ontoFile).Trim(); + var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); + if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) return true; - var job = collection.Jobs[done.Length - 1]; - File.WriteAllText(file, job.Message); + var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + if (done.Length == 0) + return true; + + var current = done[^1].Trim(); + var match = REG_REBASE_TODO().Match(current); + if (!match.Success) + return true; + + var sha = match.Groups[1].Value; + foreach (var job in collection.Jobs) + { + if (job.SHA.StartsWith(sha)) + { + File.WriteAllText(file, job.Message); + break; + } + } return true; } @@ -442,7 +521,7 @@ namespace SourceGit private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) { var args = desktop.Args; - if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal)) + if (args is not { Length: > 1 } || !args[0].Equals("--core-editor", StringComparison.Ordinal)) return false; var file = args[1]; @@ -452,8 +531,8 @@ namespace SourceGit return true; } - var editor = new Views.StandaloneCommitMessageEditor(); - editor.SetFile(file); + var editor = new Views.CommitMessageEditor(); + editor.AsStandalone(file); desktop.MainWindow = editor; return true; } @@ -478,23 +557,54 @@ namespace SourceGit private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) { - Native.OS.SetupEnternalTools(); + Native.OS.SetupExternalTools(); Models.AvatarManager.Instance.Start(); string startupRepo = null; if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) startupRepo = desktop.Args[0]; + var pref = ViewModels.Preferences.Instance; + pref.SetCanModify(); + _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; #if !DISABLE_UPDATE_DETECTION - var pref = ViewModels.Preferences.Instance; if (pref.ShouldCheck4UpdateOnStartup()) Check4Update(); #endif } + private void TryOpenRepository(string repo) + { + if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) + { + var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd(); + if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) + { + Dispatcher.UIThread.Invoke(() => + { + var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); + ViewModels.Welcome.Instance.Refresh(); + _launcher?.OpenRepositoryInTab(node, null); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd }) + wnd.BringToTop(); + }); + + return; + } + } + + Dispatcher.UIThread.Invoke(() => + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher }) + launcher.BringToTop(); + }); + } + private void Check4Update(bool manually = false) { Task.Run(async () => @@ -540,11 +650,7 @@ namespace SourceGit { Dispatcher.UIThread.Post(() => { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) - { - var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } }; - dialog.ShowDialog(owner); - } + ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true); }); } @@ -574,12 +680,24 @@ namespace SourceGit prevChar = c; } - trimmed.Add(sb.ToString()); + var name = sb.ToString(); + if (name.Contains('#', StringComparison.Ordinal)) + { + if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) && + !name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal)) + continue; + } + + trimmed.Add(name); } return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; } + [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")] + private static partial Regex REG_REBASE_TODO(); + + private Models.IpcChannel _ipcChannel = null; private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _themeOverrides = null; diff --git a/src/App.manifest b/src/App.manifest index b3bc3bdf..11a2ff11 100644 --- a/src/App.manifest +++ b/src/App.manifest @@ -1,7 +1,7 @@  diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs index b2aa803d..210eb4b2 100644 --- a/src/Commands/Add.cs +++ b/src/Commands/Add.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Add : Command { @@ -12,20 +9,11 @@ namespace SourceGit.Commands Args = includeUntracked ? "add ." : "add -u ."; } - public Add(string repo, List changes) + public Add(string repo, Models.Change change) { WorkingDirectory = repo; Context = repo; - - var builder = new StringBuilder(); - builder.Append("add --"); - foreach (var c in changes) - { - builder.Append(" \""); - builder.Append(c); - builder.Append("\""); - } - Args = builder.ToString(); + Args = $"add -- \"{change.Path}\""; } public Add(string repo, string pathspecFromFile) diff --git a/src/Commands/Archive.cs b/src/Commands/Archive.cs index d4f6241c..5e0919f7 100644 --- a/src/Commands/Archive.cs +++ b/src/Commands/Archive.cs @@ -1,23 +1,12 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Archive : Command { - public Archive(string repo, string revision, string saveTo, Action outputHandler) + public Archive(string repo, string revision, string saveTo) { WorkingDirectory = repo; Context = repo; Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}"; - TraitErrorAsOutput = true; - _outputHandler = outputHandler; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/AssumeUnchanged.cs b/src/Commands/AssumeUnchanged.cs index 1898122a..28f78280 100644 --- a/src/Commands/AssumeUnchanged.cs +++ b/src/Commands/AssumeUnchanged.cs @@ -1,75 +1,14 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Commands +namespace SourceGit.Commands { - public partial class AssumeUnchanged + public class AssumeUnchanged : Command { - [GeneratedRegex(@"^(\w)\s+(.+)$")] - private static partial Regex REG_PARSE(); - - class ViewCommand : Command + public AssumeUnchanged(string repo, string file, bool bAdd) { - public ViewCommand(string repo) - { - WorkingDirectory = repo; - Args = "ls-files -v"; - RaiseError = false; - } + var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged"; - public List Result() - { - Exec(); - return _outs; - } - - protected override void OnReadline(string line) - { - var match = REG_PARSE().Match(line); - if (!match.Success) - return; - - if (match.Groups[1].Value == "h") - { - _outs.Add(match.Groups[2].Value); - } - } - - private readonly List _outs = new List(); + WorkingDirectory = repo; + Context = repo; + Args = $"update-index {mode} -- \"{file}\""; } - - class ModCommand : Command - { - public ModCommand(string repo, string file, bool bAdd) - { - var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged"; - - WorkingDirectory = repo; - Context = repo; - Args = $"update-index {mode} -- \"{file}\""; - } - } - - public AssumeUnchanged(string repo) - { - _repo = repo; - } - - public List View() - { - return new ViewCommand(_repo).Result(); - } - - public void Add(string file) - { - new ModCommand(_repo, file, true).Exec(); - } - - public void Remove(string file) - { - new ModCommand(_repo, file, false).Exec(); - } - - private readonly string _repo; } } diff --git a/src/Commands/Bisect.cs b/src/Commands/Bisect.cs new file mode 100644 index 00000000..a3bf1a97 --- /dev/null +++ b/src/Commands/Bisect.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Commands +{ + public class Bisect : Command + { + public Bisect(string repo, string subcmd) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = $"bisect {subcmd}"; + } + } +} diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 291249be..1fc51fa4 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -21,10 +21,17 @@ namespace SourceGit.Commands public Models.BlameData Result() { - var succ = Exec(); - if (!succ) + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return _result; + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - return new Models.BlameData(); + ParseLine(line); + + if (_result.IsBinary) + break; } if (_needUnifyCommitSHA) @@ -42,14 +49,9 @@ namespace SourceGit.Commands return _result; } - protected override void OnReadline(string line) + private void ParseLine(string line) { - if (_result.IsBinary) - return; - if (string.IsNullOrEmpty(line)) - return; - - if (line.IndexOf('\0', StringComparison.Ordinal) >= 0) + if (line.Contains('\0', StringComparison.Ordinal)) { _result.IsBinary = true; _result.LineInfos.Clear(); @@ -87,7 +89,7 @@ namespace SourceGit.Commands private readonly Models.BlameData _result = new Models.BlameData(); private readonly StringBuilder _content = new StringBuilder(); - private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly; + private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly; private string _lastSHA = string.Empty; private bool _needUnifyCommitSHA = false; private int _minSHALen = 64; diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 2dc8a98d..0d1b1f8f 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public static class Branch { @@ -11,29 +13,40 @@ return cmd.ReadToEnd().StdOut.Trim(); } - public static bool Create(string repo, string name, string basedOn) + public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log) { + var builder = new StringBuilder(); + builder.Append("branch "); + if (force) + builder.Append("-f "); + builder.Append(name); + builder.Append(" "); + builder.Append(basedOn); + var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"branch {name} {basedOn}"; + cmd.Args = builder.ToString(); + cmd.Log = log; return cmd.Exec(); } - public static bool Rename(string repo, string name, string to) + public static bool Rename(string repo, string name, string to, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"branch -M {name} {to}"; + cmd.Log = log; return cmd.Exec(); } - public static bool SetUpstream(string repo, string name, string upstream) + public static bool SetUpstream(string repo, string name, string upstream, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; + cmd.Log = log; if (string.IsNullOrEmpty(upstream)) cmd.Args = $"branch {name} --unset-upstream"; @@ -43,25 +56,27 @@ return cmd.Exec(); } - public static bool DeleteLocal(string repo, string name) + public static bool DeleteLocal(string repo, string name, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"branch -D {name}"; + cmd.Log = log; return cmd.Exec(); } - public static bool DeleteRemote(string repo, string remote, string name) + public static bool DeleteRemote(string repo, string remote, string name, Models.ICommandLog log) { bool exists = new Remote(repo).HasBranch(remote, name); if (exists) - return new Push(repo, remote, $"refs/heads/{name}", true).Exec(); + return new Push(repo, remote, $"refs/heads/{name}", true) { Log = log }.Exec(); var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"branch -D -r {remote}/{name}"; + cmd.Log = log; return cmd.Exec(); } } diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index 306d62ff..d2876740 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; namespace SourceGit.Commands @@ -12,19 +11,37 @@ namespace SourceGit.Commands Context = repo; } - public bool Branch(string branch, Action onProgress) + public bool Branch(string branch, bool force) { - Args = $"checkout --recurse-submodules --progress {branch}"; - TraitErrorAsOutput = true; - _outputHandler = onProgress; + var builder = new StringBuilder(); + builder.Append("checkout --progress "); + if (force) + builder.Append("--force "); + builder.Append(branch); + + Args = builder.ToString(); return Exec(); } - public bool Branch(string branch, string basedOn, Action onProgress) + public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite) { - Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}"; - TraitErrorAsOutput = true; - _outputHandler = onProgress; + var builder = new StringBuilder(); + builder.Append("checkout --progress "); + if (force) + builder.Append("--force "); + builder.Append(allowOverwrite ? "-B " : "-b "); + builder.Append(branch); + builder.Append(" "); + builder.Append(basedOn); + + Args = builder.ToString(); + return Exec(); + } + + public bool Commit(string commitId, bool force) + { + var option = force ? "--force" : string.Empty; + Args = $"checkout {option} --detach --progress {commitId}"; return Exec(); } @@ -61,20 +78,5 @@ namespace SourceGit.Commands Args = $"checkout --no-overlay {revision} -- \"{file}\""; return Exec(); } - - public bool Commit(string commitId, Action onProgress) - { - Args = $"checkout --detach --progress {commitId}"; - TraitErrorAsOutput = true; - _outputHandler = onProgress; - return Exec(); - } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private Action _outputHandler; } } diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs index a10e5873..6ed74999 100644 --- a/src/Commands/Clean.cs +++ b/src/Commands/Clean.cs @@ -1,31 +1,12 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Clean : Command { - public Clean(string repo, bool includeIgnored) + public Clean(string repo) { WorkingDirectory = repo; Context = repo; - Args = includeIgnored ? "clean -qfdx" : "clean -qfd"; - } - - public Clean(string repo, List files) - { - var builder = new StringBuilder(); - builder.Append("clean -qfd --"); - foreach (var f in files) - { - builder.Append(" \""); - builder.Append(f); - builder.Append("\""); - } - - WorkingDirectory = repo; - Context = repo; - Args = builder.ToString(); + Args = "clean -qfdx"; } } } diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index f2faed14..efec264b 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -1,16 +1,11 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Clone : Command { - private readonly Action _notifyProgress; - - public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action ouputHandler) + public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs) { Context = ctx; WorkingDirectory = path; - TraitErrorAsOutput = true; SSHKey = sshKey; Args = "clone --progress --verbose "; @@ -21,13 +16,6 @@ namespace SourceGit.Commands if (!string.IsNullOrEmpty(localName)) Args += localName; - - _notifyProgress = ouputHandler; - } - - protected override void OnReadline(string line) - { - _notifyProgress?.Invoke(line); } } } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 0fef1235..975922fc 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -32,45 +32,18 @@ namespace SourceGit.Commands public string SSHKey { get; set; } = string.Empty; public string Args { get; set; } = string.Empty; public bool RaiseError { get; set; } = true; - public bool TraitErrorAsOutput { get; set; } = false; + public Models.ICommandLog Log { get; set; } = null; public bool Exec() { + Log?.AppendLine($"$ git {Args}\n"); + var start = CreateGitStartInfo(); var errs = new List(); var proc = new Process() { StartInfo = start }; - proc.OutputDataReceived += (_, e) => - { - if (e.Data != null) - OnReadline(e.Data); - }; - - proc.ErrorDataReceived += (_, e) => - { - if (string.IsNullOrEmpty(e.Data)) - { - errs.Add(string.Empty); - return; - } - - if (TraitErrorAsOutput) - OnReadline(e.Data); - - // Ignore progress messages - if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal)) - return; - if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) - return; - if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) - return; - if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) - return; - if (REG_PROGRESS().IsMatch(e.Data)) - return; - - errs.Add(e.Data); - }; + proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); + proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs); var dummy = null as Process; var dummyProcLock = new object(); @@ -97,6 +70,7 @@ namespace SourceGit.Commands if (RaiseError) Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message)); + Log?.AppendLine(string.Empty); return false; } @@ -114,6 +88,7 @@ namespace SourceGit.Commands int exitCode = proc.ExitCode; proc.Close(); + Log?.AppendLine(string.Empty); if (!CancellationToken.IsCancellationRequested && exitCode != 0) { @@ -162,11 +137,6 @@ namespace SourceGit.Commands return rs; } - protected virtual void OnReadline(string line) - { - // Implemented by derived class - } - private ProcessStartInfo CreateGitStartInfo() { var start = new ProcessStartInfo(); @@ -222,6 +192,28 @@ namespace SourceGit.Commands return start; } + private void HandleOutput(string line, List errs) + { + line ??= string.Empty; + Log?.AppendLine(line); + + // Lines to hide in error message. + if (line.Length > 0) + { + if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) || + line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) || + line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) || + line.StartsWith("Filtering content:", StringComparison.Ordinal) || + line.StartsWith("hint:", StringComparison.Ordinal)) + return; + + if (REG_PROGRESS().IsMatch(line)) + return; + } + + errs.Add(line); + } + [GeneratedRegex(@"\d+%")] private static partial Regex REG_PROGRESS(); } diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index cb086793..1585e7e3 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,19 +4,18 @@ namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool amend, bool signOff) + public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor) { _tmpFile = Path.GetTempFileName(); File.WriteAllText(_tmpFile, message); WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; Args = $"commit --allow-empty --file=\"{_tmpFile}\""; - if (amend) - Args += " --amend --no-edit"; if (signOff) Args += " --signoff"; + if (amend) + Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit"; } public bool Run() @@ -35,6 +34,6 @@ namespace SourceGit.Commands return succ; } - private string _tmpFile = string.Empty; + private readonly string _tmpFile; } } diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index c3206767..c88e087a 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -31,12 +31,19 @@ namespace SourceGit.Commands public List Result() { - Exec(); - _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal)); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return _changes; + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + ParseLine(line); + + _changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); return _changes; } - protected override void OnReadline(string line) + private void ParseLine(string line) { var match = REG_FORMAT().Match(line); if (!match.Success) diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChangesWithoutUntracked.cs index 0ba508f8..a704f313 100644 --- a/src/Commands/CountLocalChangesWithoutUntracked.cs +++ b/src/Commands/CountLocalChangesWithoutUntracked.cs @@ -8,7 +8,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain"; + Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain"; } public int Result() diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 8ae6350f..6af0a3cc 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -28,34 +28,48 @@ namespace SourceGit.Commands Context = repo; if (ignoreWhitespace) - Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}"; + else if (Models.DiffOption.IgnoreCRAtEOL) + Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; else - Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --unified={unified} {opt}"; } public Models.DiffResult Result() { - Exec(); + var rs = ReadToEnd(); + var start = 0; + var end = rs.StdOut.IndexOf('\n', start); + while (end > 0) + { + var line = rs.StdOut.Substring(start, end - start); + ParseLine(line); - if (_result.IsBinary || _result.IsLFS) + start = end + 1; + end = rs.StdOut.IndexOf('\n', start); + } + + if (start < rs.StdOut.Length) + ParseLine(rs.StdOut.Substring(start)); + + if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0) { _result.TextDiff = null; } else { ProcessInlineHighlights(); - - if (_result.TextDiff.Lines.Count == 0) - _result.TextDiff = null; - else - _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); + _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); } return _result; } - protected override void OnReadline(string line) + private void ParseLine(string line) { + if (_result.IsBinary) + return; + if (line.StartsWith("old mode ", StringComparison.Ordinal)) { _result.OldMode = line.Substring(9); @@ -80,9 +94,6 @@ namespace SourceGit.Commands return; } - if (_result.IsBinary) - return; - if (_result.IsLFS) { var ch = line[0]; @@ -94,7 +105,7 @@ namespace SourceGit.Commands } else if (line.StartsWith("-size ", StringComparison.Ordinal)) { - _result.LFSDiff.Old.Size = long.Parse(line.Substring(6)); + _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); } } else if (ch == '+') @@ -105,12 +116,12 @@ namespace SourceGit.Commands } else if (line.StartsWith("+size ", StringComparison.Ordinal)) { - _result.LFSDiff.New.Size = long.Parse(line.Substring(6)); + _result.LFSDiff.New.Size = long.Parse(line.AsSpan(6)); } } else if (line.StartsWith(" size ", StringComparison.Ordinal)) { - _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6)); + _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); } return; } @@ -140,7 +151,8 @@ namespace SourceGit.Commands _oldLine = int.Parse(match.Groups[1].Value); _newLine = int.Parse(match.Groups[2].Value); - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); + _result.TextDiff.Lines.Add(_last); } } else @@ -148,7 +160,8 @@ namespace SourceGit.Commands if (line.Length == 0) { ProcessInlineHighlights(); - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine); + _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; return; @@ -164,7 +177,8 @@ namespace SourceGit.Commands return; } - _deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); + _deleted.Add(_last); _oldLine++; } else if (ch == '+') @@ -176,7 +190,8 @@ namespace SourceGit.Commands return; } - _added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine); + _added.Add(_last); _newLine++; } else if (ch != '\\') @@ -187,7 +202,8 @@ namespace SourceGit.Commands { _oldLine = int.Parse(match.Groups[1].Value); _newLine = int.Parse(match.Groups[2].Value); - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); + _result.TextDiff.Lines.Add(_last); } else { @@ -198,11 +214,16 @@ namespace SourceGit.Commands return; } - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine); + _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; } } + else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal)) + { + _last.NoNewLineEndOfFile = true; + } } } @@ -253,6 +274,7 @@ namespace SourceGit.Commands private readonly Models.DiffResult _result = new Models.DiffResult(); private readonly List _deleted = new List(); private readonly List _added = new List(); + private Models.TextDiffLine _last = null; private int _oldLine = 0; private int _newLine = 0; } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index a279bb84..f36ca6c9 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -1,39 +1,95 @@ using System; using System.Collections.Generic; +using System.IO; + +using Avalonia.Threading; namespace SourceGit.Commands { public static class Discard { - public static void All(string repo, bool includeIgnored) + /// + /// Discard all local changes (unstaged & staged) + /// + /// + /// + /// + public static void All(string repo, bool includeIgnored, Models.ICommandLog log) { - new Restore(repo).Exec(); - new Clean(repo, includeIgnored).Exec(); + var changes = new QueryLocalChanges(repo).Result(); + try + { + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Untracked || + c.WorkTree == Models.ChangeState.Added || + c.Index == Models.ChangeState.Added || + c.Index == Models.ChangeState.Renamed) + { + var fullPath = Path.Combine(repo, c.Path); + if (Directory.Exists(fullPath)) + Directory.Delete(fullPath, true); + else + File.Delete(fullPath); + } + } + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + }); + } + + new Reset(repo, "HEAD", "--hard") { Log = log }.Exec(); + + if (includeIgnored) + new Clean(repo) { Log = log }.Exec(); } - public static void Changes(string repo, List changes) + /// + /// Discard selected changes (only unstaged). + /// + /// + /// + /// + public static void Changes(string repo, List changes, Models.ICommandLog log) { - var needClean = new List(); - var needCheckout = new List(); + var restores = new List(); - foreach (var c in changes) + try { - if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) - needClean.Add(c.Path); - else - needCheckout.Add(c.Path); + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) + { + var fullPath = Path.Combine(repo, c.Path); + if (Directory.Exists(fullPath)) + Directory.Delete(fullPath, true); + else + File.Delete(fullPath); + } + else + { + restores.Add(c.Path); + } + } + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + }); } - for (int i = 0; i < needClean.Count; i += 10) + if (restores.Count > 0) { - var count = Math.Min(10, needClean.Count - i); - new Clean(repo, needClean.GetRange(i, count)).Exec(); - } - - for (int i = 0; i < needCheckout.Count; i += 10) - { - var count = Math.Min(10, needCheckout.Count - i); - new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules").Exec(); + var pathSpecFile = Path.GetTempFileName(); + File.WriteAllLines(pathSpecFile, restores); + new Restore(repo, pathSpecFile, false) { Log = log }.Exec(); + File.Delete(pathSpecFile); } } } diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs index 000c8fd1..e59bc068 100644 --- a/src/Commands/ExecuteCustomAction.cs +++ b/src/Commands/ExecuteCustomAction.cs @@ -27,7 +27,7 @@ namespace SourceGit.Commands } } - public static void RunAndWait(string repo, string file, string args, Action outputHandler) + public static void RunAndWait(string repo, string file, string args, Models.ICommandLog log) { var start = new ProcessStartInfo(); start.FileName = file; @@ -40,20 +40,22 @@ namespace SourceGit.Commands start.StandardErrorEncoding = Encoding.UTF8; start.WorkingDirectory = repo; + log?.AppendLine($"$ {file} {args}\n"); + var proc = new Process() { StartInfo = start }; var builder = new StringBuilder(); proc.OutputDataReceived += (_, e) => { if (e.Data != null) - outputHandler?.Invoke(e.Data); + log?.AppendLine(e.Data); }; proc.ErrorDataReceived += (_, e) => { if (e.Data != null) { - outputHandler?.Invoke(e.Data); + log?.AppendLine(e.Data); builder.AppendLine(e.Data); } }; diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 06ae8cb6..edf2a6dd 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -1,15 +1,11 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Fetch : Command { - public Fetch(string repo, string remote, bool noTags, bool force, Action outputHandler) + public Fetch(string repo, string remote, bool noTags, bool force) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); Args = "fetch --progress --verbose "; @@ -24,21 +20,12 @@ namespace SourceGit.Commands Args += remote; } - public Fetch(string repo, Models.Branch local, Models.Branch remote, Action outputHandler) + public Fetch(string repo, Models.Branch local, Models.Branch remote) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey"); Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs index b3ec2e4a..bf850d60 100644 --- a/src/Commands/FormatPatch.cs +++ b/src/Commands/FormatPatch.cs @@ -6,6 +6,7 @@ { WorkingDirectory = repo; Context = repo; + Editor = EditorType.None; Args = $"format-patch {commit} -1 --output=\"{saveTo}\""; } } diff --git a/src/Commands/GC.cs b/src/Commands/GC.cs index 393b915e..0b27f487 100644 --- a/src/Commands/GC.cs +++ b/src/Commands/GC.cs @@ -1,23 +1,12 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class GC : Command { - public GC(string repo, Action outputHandler) + public GC(string repo) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; Args = "gc --prune=now"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 5e05ed83..1d33fa3a 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -1,52 +1,12 @@ -using System; -using System.Collections.Generic; - +using System.Text; using Avalonia.Threading; namespace SourceGit.Commands { public static class GitFlow { - public class BranchDetectResult + public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log) { - public bool IsGitFlowBranch { get; set; } = false; - public string Type { get; set; } = string.Empty; - public string Prefix { get; set; } = string.Empty; - } - - public static bool IsEnabled(string repo, List branches) - { - var localBrancheNames = new HashSet(); - foreach (var branch in branches) - { - if (branch.IsLocal) - localBrancheNames.Add(branch.Name); - } - - var config = new Config(repo).ListAll(); - if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master)) - return false; - - if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop)) - return false; - - return config.ContainsKey("gitflow.prefix.feature") && - config.ContainsKey("gitflow.prefix.release") && - config.ContainsKey("gitflow.prefix.hotfix"); - } - - public static bool Init(string repo, List branches, string master, string develop, string feature, string release, string hotfix, string version) - { - var current = branches.Find(x => x.IsCurrent); - - var masterBranch = branches.Find(x => x.Name == master); - if (masterBranch == null && current != null) - Branch.Create(repo, master, current.Head); - - var devBranch = branches.Find(x => x.Name == develop); - if (devBranch == null && current != null) - Branch.Create(repo, develop, current.Head); - var config = new Config(repo); config.Set("gitflow.branch.master", master); config.Set("gitflow.branch.develop", develop); @@ -61,104 +21,72 @@ namespace SourceGit.Commands init.WorkingDirectory = repo; init.Context = repo; init.Args = "flow init -d"; + init.Log = log; return init.Exec(); } - public static string GetPrefix(string repo, string type) + public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) { - return new Config(repo).Get($"gitflow.prefix.{type}"); - } - - public static BranchDetectResult DetectType(string repo, List branches, string branch) - { - var rs = new BranchDetectResult(); - var localBrancheNames = new HashSet(); - foreach (var b in branches) - { - if (b.IsLocal) - localBrancheNames.Add(b.Name); - } - - var config = new Config(repo).ListAll(); - if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master)) - return rs; - - if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop)) - return rs; - - if (!config.TryGetValue("gitflow.prefix.feature", out var feature) || - !config.TryGetValue("gitflow.prefix.release", out var release) || - !config.TryGetValue("gitflow.prefix.hotfix", out var hotfix)) - return rs; - - if (branch.StartsWith(feature, StringComparison.Ordinal)) - { - rs.IsGitFlowBranch = true; - rs.Type = "feature"; - rs.Prefix = feature; - } - else if (branch.StartsWith(release, StringComparison.Ordinal)) - { - rs.IsGitFlowBranch = true; - rs.Type = "release"; - rs.Prefix = release; - } - else if (branch.StartsWith(hotfix, StringComparison.Ordinal)) - { - rs.IsGitFlowBranch = true; - rs.Type = "hotfix"; - rs.Prefix = hotfix; - } - - return rs; - } - - public static bool Start(string repo, string type, string name) - { - if (!SUPPORTED_BRANCH_TYPES.Contains(type)) - { - Dispatcher.UIThread.Post(() => - { - App.RaiseException(repo, "Bad branch type!!!"); - }); - - return false; - } - var start = new Command(); start.WorkingDirectory = repo; start.Context = repo; - start.Args = $"flow {type} start {name}"; + + switch (type) + { + case Models.GitFlowBranchType.Feature: + start.Args = $"flow feature start {name}"; + break; + case Models.GitFlowBranchType.Release: + start.Args = $"flow release start {name}"; + break; + case Models.GitFlowBranchType.Hotfix: + start.Args = $"flow hotfix start {name}"; + break; + default: + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); + return false; + } + + start.Log = log; return start.Exec(); } - public static bool Finish(string repo, string type, string name, bool keepBranch) + public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) { - if (!SUPPORTED_BRANCH_TYPES.Contains(type)) - { - Dispatcher.UIThread.Post(() => - { - App.RaiseException(repo, "Bad branch type!!!"); - }); + var builder = new StringBuilder(); + builder.Append("flow "); - return false; + switch (type) + { + case Models.GitFlowBranchType.Feature: + builder.Append("feature"); + break; + case Models.GitFlowBranchType.Release: + builder.Append("release"); + break; + case Models.GitFlowBranchType.Hotfix: + builder.Append("hotfix"); + break; + default: + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); + return false; } - var option = keepBranch ? "-k" : string.Empty; + builder.Append(" finish "); + if (squash) + builder.Append("--squash "); + if (push) + builder.Append("--push "); + if (keepBranch) + builder.Append("-k "); + builder.Append(name); + var finish = new Command(); finish.WorkingDirectory = repo; finish.Context = repo; - finish.Args = $"flow {type} finish {option} {name}"; + finish.Args = builder.ToString(); + finish.Log = log; return finish.Exec(); } - - private static readonly List SUPPORTED_BRANCH_TYPES = new List() - { - "feature", - "release", - "bugfix", - "hotfix", - "support", - }; } } diff --git a/src/Commands/GitIgnore.cs b/src/Commands/GitIgnore.cs index e666eba6..8b351f5e 100644 --- a/src/Commands/GitIgnore.cs +++ b/src/Commands/GitIgnore.cs @@ -8,7 +8,14 @@ namespace SourceGit.Commands { var file = Path.Combine(repo, ".gitignore"); if (!File.Exists(file)) + { File.WriteAllLines(file, [pattern]); + return; + } + + var org = File.ReadAllText(file); + if (!org.EndsWith('\n')) + File.AppendAllLines(file, ["", pattern]); else File.AppendAllLines(file, [pattern]); } diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs index de59b5a4..af8f54bb 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -11,7 +11,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\""; + Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- \"{path}\""; RaiseError = false; } diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs index 13bc12ac..9b243451 100644 --- a/src/Commands/IsConflictResolved.cs +++ b/src/Commands/IsConflictResolved.cs @@ -10,5 +10,10 @@ Context = repo; Args = $"diff -a --ignore-cr-at-eol --check {opt}"; } + + public bool Result() + { + return ReadToEnd().IsSuccess; + } } } diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs index f7e56486..18d2ba93 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -10,23 +10,15 @@ namespace SourceGit.Commands [GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] private static partial Regex REG_LOCK(); - class SubCmd : Command + private class SubCmd : Command { - public SubCmd(string repo, string args, Action onProgress) + public SubCmd(string repo, string args, Models.ICommandLog log) { WorkingDirectory = repo; Context = repo; Args = args; - TraitErrorAsOutput = true; - _outputHandler = onProgress; + Log = log; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } public LFS(string repo) @@ -44,35 +36,35 @@ namespace SourceGit.Commands return content.Contains("git lfs pre-push"); } - public bool Install() + public bool Install(Models.ICommandLog log) { - return new SubCmd(_repo, "lfs install --local", null).Exec(); + return new SubCmd(_repo, "lfs install --local", log).Exec(); } - public bool Track(string pattern, bool isFilenameMode = false) + public bool Track(string pattern, bool isFilenameMode, Models.ICommandLog log) { var opt = isFilenameMode ? "--filename" : ""; - return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", null).Exec(); + return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", log).Exec(); } - public void Fetch(string remote, Action outputHandler) + public void Fetch(string remote, Models.ICommandLog log) { - new SubCmd(_repo, $"lfs fetch {remote}", outputHandler).Exec(); + new SubCmd(_repo, $"lfs fetch {remote}", log).Exec(); } - public void Pull(string remote, Action outputHandler) + public void Pull(string remote, Models.ICommandLog log) { - new SubCmd(_repo, $"lfs pull {remote}", outputHandler).Exec(); + new SubCmd(_repo, $"lfs pull {remote}", log).Exec(); } - public void Push(string remote, Action outputHandler) + public void Push(string remote, Models.ICommandLog log) { - new SubCmd(_repo, $"lfs push {remote}", outputHandler).Exec(); + new SubCmd(_repo, $"lfs push {remote}", log).Exec(); } - public void Prune(Action outputHandler) + public void Prune(Models.ICommandLog log) { - new SubCmd(_repo, "lfs prune", outputHandler).Exec(); + new SubCmd(_repo, "lfs prune", log).Exec(); } public List Locks(string remote) @@ -101,21 +93,21 @@ namespace SourceGit.Commands return locks; } - public bool Lock(string remote, string file) + public bool Lock(string remote, string file, Models.ICommandLog log) { - return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", null).Exec(); + return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", log).Exec(); } - public bool Unlock(string remote, string file, bool force) + public bool Unlock(string remote, string file, bool force, Models.ICommandLog log) { var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", null).Exec(); + return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", log).Exec(); } - public bool Unlock(string remote, long id, bool force) + public bool Unlock(string remote, long id, bool force, Models.ICommandLog log) { var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", null).Exec(); + return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", log).Exec(); } private readonly string _repo; diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs index bd1f3653..32898593 100644 --- a/src/Commands/Merge.cs +++ b/src/Commands/Merge.cs @@ -1,26 +1,30 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; namespace SourceGit.Commands { public class Merge : Command { - public Merge(string repo, string source, string mode, Action outputHandler) + public Merge(string repo, string source, string mode, bool edit) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; - Args = $"merge --progress {source} {mode}"; + Editor = EditorType.CoreEditor; + + var builder = new StringBuilder(); + builder.Append("merge --progress "); + builder.Append(edit ? "--edit " : "--no-edit "); + builder.Append(source); + builder.Append(' '); + builder.Append(mode); + + Args = builder.ToString(); } - public Merge(string repo, List targets, bool autoCommit, string strategy, Action outputHandler) + public Merge(string repo, List targets, bool autoCommit, string strategy) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; var builder = new StringBuilder(); builder.Append("merge --progress "); @@ -37,12 +41,5 @@ namespace SourceGit.Commands Args = builder.ToString(); } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler = null; } } diff --git a/src/Commands/MergeTool.cs b/src/Commands/MergeTool.cs index f67f5e48..fc6d0d75 100644 --- a/src/Commands/MergeTool.cs +++ b/src/Commands/MergeTool.cs @@ -24,7 +24,7 @@ namespace SourceGit.Commands if (!File.Exists(toolPath)) { - Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!")); + Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!")); return false; } @@ -54,7 +54,7 @@ namespace SourceGit.Commands if (!File.Exists(toolPath)) { - Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!")); + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!")); return false; } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index 35a6289a..698fbfce 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -1,32 +1,18 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action outputHandler) + public Pull(string repo, string remote, string branch, bool useRebase) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); Args = "pull --verbose --progress "; if (useRebase) Args += "--rebase=true "; - if (noTags) - Args += "--no-tags "; - Args += $"{remote} {branch}"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index dc81f606..8a5fe33c 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -1,16 +1,11 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Push : Command { - public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force, Action onProgress) + public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force) { - _outputHandler = onProgress; - WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); Args = "push --progress --verbose "; @@ -38,12 +33,5 @@ namespace SourceGit.Commands Args += $"{remote} {refname}"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler = null; } } diff --git a/src/Commands/QueryAssumeUnchangedFiles.cs b/src/Commands/QueryAssumeUnchangedFiles.cs new file mode 100644 index 00000000..b5c23b0b --- /dev/null +++ b/src/Commands/QueryAssumeUnchangedFiles.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands +{ + public partial class QueryAssumeUnchangedFiles : Command + { + [GeneratedRegex(@"^(\w)\s+(.+)$")] + private static partial Regex REG_PARSE(); + + public QueryAssumeUnchangedFiles(string repo) + { + WorkingDirectory = repo; + Args = "ls-files -v"; + RaiseError = false; + } + + public List Result() + { + var outs = new List(); + var rs = ReadToEnd(); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_PARSE().Match(line); + if (!match.Success) + continue; + + if (match.Groups[1].Value == "h") + outs.Add(match.Groups[2].Value); + } + + return outs; + } + } +} diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 8dbc4055..f268d709 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -14,18 +14,20 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; + Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; } - public List Result() + public List Result(out int localBranchesCount) { + localBranchesCount = 0; + var branches = new List(); var rs = ReadToEnd(); if (!rs.IsSuccess) return branches; var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - var remoteBranches = new HashSet(); + var remoteHeads = new Dictionary(); foreach (var line in lines) { var b = ParseLine(line); @@ -33,14 +35,27 @@ namespace SourceGit.Commands { branches.Add(b); if (!b.IsLocal) - remoteBranches.Add(b.FullName); + remoteHeads.Add(b.FullName, b.Head); + else + localBranchesCount++; } } foreach (var b in branches) { if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream)) - b.IsUpsteamGone = !remoteBranches.Contains(b.Upstream); + { + if (remoteHeads.TryGetValue(b.Upstream, out var upstreamHead)) + { + b.IsUpstreamGone = false; + b.TrackStatus ??= new QueryTrackStatus(WorkingDirectory, b.Head, upstreamHead).Result(); + } + else + { + b.IsUpstreamGone = true; + b.TrackStatus ??= new Models.BranchTrackStatus(); + } + } } return branches; @@ -49,7 +64,7 @@ namespace SourceGit.Commands private Models.Branch ParseLine(string line) { var parts = line.Split('\0'); - if (parts.Length != 5) + if (parts.Length != 6) return null; var branch = new Models.Branch(); @@ -83,14 +98,16 @@ namespace SourceGit.Commands } branch.FullName = refName; - branch.Head = parts[1]; - branch.IsCurrent = parts[2] == "*"; - branch.Upstream = parts[3]; - branch.IsUpsteamGone = false; + branch.CommitterDate = ulong.Parse(parts[1]); + branch.Head = parts[2]; + branch.IsCurrent = parts[3] == "*"; + branch.Upstream = parts[4]; + branch.IsUpstreamGone = false; - if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal)) - branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result(); - else + if (!branch.IsLocal || + string.IsNullOrEmpty(branch.Upstream) || + string.IsNullOrEmpty(parts[5]) || + parts[5].Equals("=", StringComparison.Ordinal)) branch.TrackStatus = new Models.BranchTrackStatus(); return branch; diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs index 31fb34f8..4e99ce7a 100644 --- a/src/Commands/QueryCommitChildren.cs +++ b/src/Commands/QueryCommitChildren.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace SourceGit.Commands { @@ -14,17 +15,21 @@ namespace SourceGit.Commands public List Result() { - Exec(); - return _lines; - } + var rs = ReadToEnd(); + var outs = new List(); + if (rs.IsSuccess) + { + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.Contains(_commit)) + outs.Add(line.Substring(0, 40)); + } + } - protected override void OnReadline(string line) - { - if (line.Contains(_commit)) - _lines.Add(line.Substring(0, 40)); + return outs; } private string _commit; - private List _lines = new List(); } } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index dd3c39b4..9e1d9918 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -26,11 +26,7 @@ namespace SourceGit.Commands { search += $"-i --committer=\"{filter}\""; } - else if (method == Models.CommitSearchMethod.ByFile) - { - search += $"-- \"{filter}\""; - } - else + else if (method == Models.CommitSearchMethod.ByMessage) { var argsBuilder = new StringBuilder(); argsBuilder.Append(search); @@ -45,10 +41,18 @@ namespace SourceGit.Commands search = argsBuilder.ToString(); } + else if (method == Models.CommitSearchMethod.ByFile) + { + search += $"-- \"{filter}\""; + } + else + { + search = $"-G\"{filter}\""; + } WorkingDirectory = repo; Context = repo; - Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search; + Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {search}"; _findFirstMerged = false; } diff --git a/src/Commands/QueryCommitsForInteractiveRebase.cs b/src/Commands/QueryCommitsForInteractiveRebase.cs index 615060a5..9f238319 100644 --- a/src/Commands/QueryCommitsForInteractiveRebase.cs +++ b/src/Commands/QueryCommitsForInteractiveRebase.cs @@ -90,6 +90,6 @@ namespace SourceGit.Commands private List _commits = []; private Models.InteractiveCommit _current = null; - private string _boundary = ""; + private readonly string _boundary; } } diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs index f887859c..83d0a575 100644 --- a/src/Commands/QueryFileContent.cs +++ b/src/Commands/QueryFileContent.cs @@ -35,5 +35,39 @@ namespace SourceGit.Commands return stream; } + + public static Stream FromLFS(string repo, string oid, long size) + { + var starter = new ProcessStartInfo(); + starter.WorkingDirectory = repo; + starter.FileName = Native.OS.GitExecutable; + starter.Arguments = $"lfs smudge"; + starter.UseShellExecute = false; + starter.CreateNoWindow = true; + starter.WindowStyle = ProcessWindowStyle.Hidden; + starter.RedirectStandardInput = true; + starter.RedirectStandardOutput = true; + + var stream = new MemoryStream(); + try + { + var proc = new Process() { StartInfo = starter }; + proc.Start(); + proc.StandardInput.WriteLine("version https://git-lfs.github.com/spec/v1"); + proc.StandardInput.WriteLine($"oid sha256:{oid}"); + proc.StandardInput.WriteLine($"size {size}"); + proc.StandardOutput.BaseStream.CopyTo(stream); + proc.WaitForExit(); + proc.Close(); + + stream.Position = 0; + } + catch (Exception e) + { + App.RaiseException(repo, $"Failed to query file content: {e}"); + } + + return stream; + } } } diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs index 9016d826..30af7715 100644 --- a/src/Commands/QueryFileSize.cs +++ b/src/Commands/QueryFileSize.cs @@ -16,9 +16,6 @@ namespace SourceGit.Commands public long Result() { - if (_result != 0) - return _result; - var rs = ReadToEnd(); if (rs.IsSuccess) { @@ -29,7 +26,5 @@ namespace SourceGit.Commands return 0; } - - private readonly long _result = 0; } } diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 9458e5f5..788ed617 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; +using Avalonia.Threading; + namespace SourceGit.Commands { public partial class QueryLocalChanges : Command @@ -18,139 +21,145 @@ namespace SourceGit.Commands public List Result() { - Exec(); - return _changes; - } - - protected override void OnReadline(string line) - { - var match = REG_FORMAT().Match(line); - if (!match.Success) - return; - - var change = new Models.Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; - - switch (status) + var outs = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) { - case " M": - change.Set(Models.ChangeState.None, Models.ChangeState.Modified); - break; - case " T": - change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged); - break; - case " A": - change.Set(Models.ChangeState.None, Models.ChangeState.Added); - break; - case " D": - change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); - break; - case " R": - change.Set(Models.ChangeState.None, Models.ChangeState.Renamed); - break; - case " C": - change.Set(Models.ChangeState.None, Models.ChangeState.Copied); - break; - case "M": - change.Set(Models.ChangeState.Modified); - break; - case "MM": - change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); - break; - case "MT": - change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged); - break; - case "MD": - change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); - break; - case "T": - change.Set(Models.ChangeState.TypeChanged); - break; - case "TM": - change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified); - break; - case "TT": - change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged); - break; - case "TD": - change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted); - break; - case "A": - change.Set(Models.ChangeState.Added); - break; - case "AM": - change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); - break; - case "AT": - change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged); - break; - case "AD": - change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); - break; - case "D": - change.Set(Models.ChangeState.Deleted); - break; - case "R": - change.Set(Models.ChangeState.Renamed); - break; - case "RM": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); - break; - case "RT": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged); - break; - case "RD": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); - break; - case "C": - change.Set(Models.ChangeState.Copied); - break; - case "CM": - change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); - break; - case "CT": - change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged); - break; - case "CD": - change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); - break; - case "DR": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed); - break; - case "DC": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied); - break; - case "DD": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted); - break; - case "AU": - change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged); - break; - case "UD": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted); - break; - case "UA": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added); - break; - case "DU": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged); - break; - case "AA": - change.Set(Models.ChangeState.Added, Models.ChangeState.Added); - break; - case "UU": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged); - break; - case "??": - change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked); - break; - default: - return; + Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr)); + return outs; } - _changes.Add(change); - } + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT().Match(line); + if (!match.Success) + continue; - private readonly List _changes = new List(); + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; + + switch (status) + { + case " M": + change.Set(Models.ChangeState.None, Models.ChangeState.Modified); + break; + case " T": + change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged); + break; + case " A": + change.Set(Models.ChangeState.None, Models.ChangeState.Added); + break; + case " D": + change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); + break; + case " R": + change.Set(Models.ChangeState.None, Models.ChangeState.Renamed); + break; + case " C": + change.Set(Models.ChangeState.None, Models.ChangeState.Copied); + break; + case "M": + change.Set(Models.ChangeState.Modified); + break; + case "MM": + change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); + break; + case "MT": + change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged); + break; + case "MD": + change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); + break; + case "T": + change.Set(Models.ChangeState.TypeChanged); + break; + case "TM": + change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified); + break; + case "TT": + change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged); + break; + case "TD": + change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted); + break; + case "A": + change.Set(Models.ChangeState.Added); + break; + case "AM": + change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); + break; + case "AT": + change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged); + break; + case "AD": + change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); + break; + case "D": + change.Set(Models.ChangeState.Deleted); + break; + case "R": + change.Set(Models.ChangeState.Renamed); + break; + case "RM": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); + break; + case "RT": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged); + break; + case "RD": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); + break; + case "C": + change.Set(Models.ChangeState.Copied); + break; + case "CM": + change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); + break; + case "CT": + change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged); + break; + case "CD": + change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); + break; + case "DD": + change.ConflictReason = Models.ConflictReason.BothDeleted; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "AU": + change.ConflictReason = Models.ConflictReason.AddedByUs; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "UD": + change.ConflictReason = Models.ConflictReason.DeletedByThem; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "UA": + change.ConflictReason = Models.ConflictReason.AddedByThem; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "DU": + change.ConflictReason = Models.ConflictReason.DeletedByUs; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "AA": + change.ConflictReason = Models.ConflictReason.BothAdded; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "UU": + change.ConflictReason = Models.ConflictReason.BothModified; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "??": + change.Set(Models.ChangeState.None, Models.ChangeState.Untracked); + break; + } + + if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None) + outs.Add(change); + } + + return outs; + } } } diff --git a/src/Commands/QueryRemotes.cs b/src/Commands/QueryRemotes.cs index b5b41b4a..7afec74d 100644 --- a/src/Commands/QueryRemotes.cs +++ b/src/Commands/QueryRemotes.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; namespace SourceGit.Commands @@ -17,27 +18,31 @@ namespace SourceGit.Commands public List Result() { - Exec(); - return _loaded; - } + var outs = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return outs; - protected override void OnReadline(string line) - { - var match = REG_REMOTE().Match(line); - if (!match.Success) - return; - - var remote = new Models.Remote() + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - Name = match.Groups[1].Value, - URL = match.Groups[2].Value, - }; + var match = REG_REMOTE().Match(line); + if (!match.Success) + continue; - if (_loaded.Find(x => x.Name == remote.Name) != null) - return; - _loaded.Add(remote); + var remote = new Models.Remote() + { + Name = match.Groups[1].Value, + URL = match.Groups[2].Value, + }; + + if (outs.Find(x => x.Name == remote.Name) != null) + continue; + + outs.Add(remote); + } + + return outs; } - - private readonly List _loaded = new List(); } } diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs index cfea5e35..78980401 100644 --- a/src/Commands/QueryStagedChangesWithAmend.cs +++ b/src/Commands/QueryStagedChangesWithAmend.cs @@ -6,87 +6,87 @@ namespace SourceGit.Commands { public partial class QueryStagedChangesWithAmend : Command { - [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")] + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")] private static partial Regex REG_FORMAT1(); [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")] private static partial Regex REG_FORMAT2(); - public QueryStagedChangesWithAmend(string repo) + public QueryStagedChangesWithAmend(string repo, string parent) { WorkingDirectory = repo; Context = repo; - Args = "diff-index --cached -M HEAD^"; + Args = $"diff-index --cached -M {parent}"; + _parent = parent; } public List Result() { var rs = ReadToEnd(); - if (rs.IsSuccess) + if (!rs.IsSuccess) + return []; + + var changes = new List(); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - var changes = new List(); - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) + var match = REG_FORMAT2().Match(line); + if (match.Success) { - var match = REG_FORMAT2().Match(line); - if (match.Success) + var change = new Models.Change() { - var change = new Models.Change() + Path = match.Groups[3].Value, + DataForAmend = new Models.ChangeDataForAmend() { - Path = match.Groups[3].Value, - DataForAmend = new Models.ChangeDataForAmend() - { - FileMode = match.Groups[1].Value, - ObjectHash = match.Groups[2].Value, - }, - }; - change.Set(Models.ChangeState.Renamed); - changes.Add(change); - continue; - } - - match = REG_FORMAT1().Match(line); - if (match.Success) - { - var change = new Models.Change() - { - Path = match.Groups[4].Value, - DataForAmend = new Models.ChangeDataForAmend() - { - FileMode = match.Groups[1].Value, - ObjectHash = match.Groups[2].Value, - }, - }; - - var type = match.Groups[3].Value; - switch (type) - { - case "A": - change.Set(Models.ChangeState.Added); - break; - case "C": - change.Set(Models.ChangeState.Copied); - break; - case "D": - change.Set(Models.ChangeState.Deleted); - break; - case "M": - change.Set(Models.ChangeState.Modified); - break; - case "T": - change.Set(Models.ChangeState.TypeChanged); - break; - case "U": - change.Set(Models.ChangeState.Unmerged); - break; - } - changes.Add(change); - } + FileMode = match.Groups[1].Value, + ObjectHash = match.Groups[2].Value, + ParentSHA = _parent, + }, + }; + change.Set(Models.ChangeState.Renamed); + changes.Add(change); + continue; } - return changes; + match = REG_FORMAT1().Match(line); + if (match.Success) + { + var change = new Models.Change() + { + Path = match.Groups[4].Value, + DataForAmend = new Models.ChangeDataForAmend() + { + FileMode = match.Groups[1].Value, + ObjectHash = match.Groups[2].Value, + ParentSHA = _parent, + }, + }; + + var type = match.Groups[3].Value; + switch (type) + { + case "A": + change.Set(Models.ChangeState.Added); + break; + case "C": + change.Set(Models.ChangeState.Copied); + break; + case "D": + change.Set(Models.ChangeState.Deleted); + break; + case "M": + change.Set(Models.ChangeState.Modified); + break; + case "T": + change.Set(Models.ChangeState.TypeChanged); + break; + } + changes.Add(change); + } } - return []; + return changes; } + + private readonly string _parent; } } diff --git a/src/Commands/QueryStashChanges.cs b/src/Commands/QueryStashChanges.cs deleted file mode 100644 index 1f6c5b4f..00000000 --- a/src/Commands/QueryStashChanges.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Commands -{ - /// - /// Query stash changes. Requires git >= 2.32.0 - /// - public partial class QueryStashChanges : Command - { - [GeneratedRegex(@"^([MADC])\s+(.+)$")] - private static partial Regex REG_FORMAT(); - [GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")] - private static partial Regex REG_RENAME_FORMAT(); - - public QueryStashChanges(string repo, string stash) - { - WorkingDirectory = repo; - Context = repo; - Args = $"stash show -u --name-status \"{stash}\""; - } - - public List Result() - { - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return []; - - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - var outs = new List(); - foreach (var line in lines) - { - var match = REG_FORMAT().Match(line); - if (!match.Success) - { - match = REG_RENAME_FORMAT().Match(line); - if (match.Success) - { - var renamed = new Models.Change() { Path = match.Groups[1].Value }; - renamed.Set(Models.ChangeState.Renamed); - outs.Add(renamed); - } - - continue; - } - - var change = new Models.Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; - - switch (status[0]) - { - case 'M': - change.Set(Models.ChangeState.Modified); - outs.Add(change); - break; - case 'A': - change.Set(Models.ChangeState.Added); - outs.Add(change); - break; - case 'D': - change.Set(Models.ChangeState.Deleted); - outs.Add(change); - break; - case 'C': - change.Set(Models.ChangeState.Copied); - outs.Add(change); - break; - } - } - - outs.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal)); - return outs; - } - } -} diff --git a/src/Commands/QueryStashes.cs b/src/Commands/QueryStashes.cs index dd5d10cc..2b8987b6 100644 --- a/src/Commands/QueryStashes.cs +++ b/src/Commands/QueryStashes.cs @@ -9,52 +9,60 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s"; + Args = $"stash list -z --no-show-signature --format=\"%H%n%P%n%ct%n%gd%n%B\""; } public List Result() { - Exec(); - return _stashes; - } + var outs = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return outs; - protected override void OnReadline(string line) - { - switch (_nextLineIdx) + var items = rs.StdOut.Split('\0', StringSplitOptions.RemoveEmptyEntries); + foreach (var item in items) { - case 0: - _current = new Models.Stash() { SHA = line }; - _stashes.Add(_current); - break; - case 1: - ParseParent(line); - break; - case 2: - _current.Time = ulong.Parse(line); - break; - case 3: - _current.Name = line; - break; - case 4: - _current.Message = line; - break; + var current = new Models.Stash(); + + var nextPartIdx = 0; + var start = 0; + var end = item.IndexOf('\n', start); + while (end > 0 && nextPartIdx < 4) + { + var line = item.Substring(start, end - start); + + switch (nextPartIdx) + { + case 0: + current.SHA = line; + break; + case 1: + if (line.Length > 6) + current.Parents.AddRange(line.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + break; + case 2: + current.Time = ulong.Parse(line); + break; + case 3: + current.Name = line; + break; + } + + nextPartIdx++; + + start = end + 1; + if (start >= item.Length - 1) + break; + + end = item.IndexOf('\n', start); + } + + if (start < item.Length) + current.Message = item.Substring(start); + + outs.Add(current); } - - _nextLineIdx++; - if (_nextLineIdx > 4) - _nextLineIdx = 0; + return outs; } - - private void ParseParent(string data) - { - if (data.Length < 8) - return; - - _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); - } - - private readonly List _stashes = new List(); - private Models.Stash _current = null; - private int _nextLineIdx = 0; } } diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 6016b0be..663c0ea0 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -6,12 +7,12 @@ namespace SourceGit.Commands { public partial class QuerySubmodules : Command { - [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")] - private static partial Regex REG_FORMAT1(); - [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")] - private static partial Regex REG_FORMAT2(); - [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] private static partial Regex REG_FORMAT_STATUS(); + [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + private static partial Regex REG_FORMAT_DIRTY(); + [GeneratedRegex(@"^submodule\.(\S*)\.(\w+)=(.*)$")] + private static partial Regex REG_FORMAT_MODULE_INFO(); public QuerySubmodules(string repo) { @@ -25,52 +26,117 @@ namespace SourceGit.Commands var submodules = new List(); var rs = ReadToEnd(); - var builder = new StringBuilder(); - var lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var map = new Dictionary(); + var needCheckLocalChanges = false; foreach (var line in lines) { - var match = REG_FORMAT1().Match(line); + var match = REG_FORMAT_STATUS().Match(line); if (match.Success) { - var path = match.Groups[1].Value; - builder.Append($"\"{path}\" "); - submodules.Add(new Models.Submodule() { Path = path }); - continue; - } + var stat = match.Groups[1].Value; + var sha = match.Groups[2].Value; + var path = match.Groups[3].Value; - match = REG_FORMAT2().Match(line); - if (match.Success) - { - var path = match.Groups[1].Value; - builder.Append($"\"{path}\" "); - submodules.Add(new Models.Submodule() { Path = path }); + var module = new Models.Submodule() { Path = path, SHA = sha }; + switch (stat[0]) + { + case '-': + module.Status = Models.SubmoduleStatus.NotInited; + break; + case '+': + module.Status = Models.SubmoduleStatus.RevisionChanged; + break; + case 'U': + module.Status = Models.SubmoduleStatus.Unmerged; + break; + default: + module.Status = Models.SubmoduleStatus.Normal; + needCheckLocalChanges = true; + break; + } + + map.Add(path, module); + submodules.Add(module); } } if (submodules.Count > 0) { - Args = $"--no-optional-locks status -uno --porcelain -- {builder}"; + Args = "config --file .gitmodules --list"; + rs = ReadToEnd(); + if (rs.IsSuccess) + { + var modules = new Dictionary(); + lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT_MODULE_INFO().Match(line); + if (match.Success) + { + var name = match.Groups[1].Value; + var key = match.Groups[2].Value; + var val = match.Groups[3].Value; + + if (!modules.TryGetValue(name, out var m)) + { + m = new ModuleInfo(); + modules.Add(name, m); + } + + if (key.Equals("path", StringComparison.Ordinal)) + m.Path = val; + else if (key.Equals("url", StringComparison.Ordinal)) + m.URL = val; + } + } + + foreach (var kv in modules) + { + if (map.TryGetValue(kv.Value.Path, out var m)) + m.URL = kv.Value.URL; + } + } + } + + if (needCheckLocalChanges) + { + var builder = new StringBuilder(); + foreach (var kv in map) + { + if (kv.Value.Status == Models.SubmoduleStatus.Normal) + { + builder.Append('"'); + builder.Append(kv.Key); + builder.Append("\" "); + } + } + + Args = $"--no-optional-locks status --porcelain -- {builder}"; rs = ReadToEnd(); if (!rs.IsSuccess) return submodules; - var dirty = new HashSet(); - lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries); + lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { - var match = REG_FORMAT_STATUS().Match(line); + var match = REG_FORMAT_DIRTY().Match(line); if (match.Success) { var path = match.Groups[1].Value; - dirty.Add(path); + if (map.TryGetValue(path, out var m)) + m.Status = Models.SubmoduleStatus.Modified; } } - - foreach (var submodule in submodules) - submodule.IsDirty = dirty.Contains(submodule.Path); } return submodules; } + + private class ModuleInfo + { + public string Path { get; set; } = string.Empty; + public string URL { get; set; } = string.Empty; + } } } diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 73f63d8b..896d555e 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -11,7 +11,7 @@ namespace SourceGit.Commands Context = repo; WorkingDirectory = repo; - Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; + Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; } public List Result() @@ -24,17 +24,22 @@ namespace SourceGit.Commands var records = rs.StdOut.Split(_boundary, StringSplitOptions.RemoveEmptyEntries); foreach (var record in records) { - var subs = record.Split('\0', StringSplitOptions.None); - if (subs.Length != 5) + var subs = record.Split('\0'); + if (subs.Length != 6) continue; - var message = subs[4].Trim(); + var name = subs[0].Substring(10); + var message = subs[5].Trim(); + if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal)) + message = null; + tags.Add(new Models.Tag() { - Name = subs[0].Substring(10), - SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2], - CreatorDate = ulong.Parse(subs[3]), - Message = string.IsNullOrEmpty(message) ? null : message, + Name = name, + IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal), + SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3], + CreatorDate = ulong.Parse(subs[4]), + Message = message, }); } diff --git a/src/Commands/QueryUpdatableSubmodules.cs b/src/Commands/QueryUpdatableSubmodules.cs new file mode 100644 index 00000000..03f4a24d --- /dev/null +++ b/src/Commands/QueryUpdatableSubmodules.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands +{ + public partial class QueryUpdatableSubmodules : Command + { + [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] + private static partial Regex REG_FORMAT_STATUS(); + + public QueryUpdatableSubmodules(string repo) + { + WorkingDirectory = repo; + Context = repo; + Args = "submodule status"; + } + + public List Result() + { + var submodules = new List(); + var rs = ReadToEnd(); + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT_STATUS().Match(line); + if (match.Success) + { + var stat = match.Groups[1].Value; + var path = match.Groups[3].Value; + if (!stat.StartsWith(' ')) + submodules.Add(path); + } + } + + return submodules; + } + } +} diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs index da272135..6a54533b 100644 --- a/src/Commands/Reset.cs +++ b/src/Commands/Reset.cs @@ -1,33 +1,7 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Reset : Command { - public Reset(string repo) - { - WorkingDirectory = repo; - Context = repo; - Args = "reset"; - } - - public Reset(string repo, List changes) - { - WorkingDirectory = repo; - Context = repo; - - var builder = new StringBuilder(); - builder.Append("reset --"); - foreach (var c in changes) - { - builder.Append(" \""); - builder.Append(c.Path); - builder.Append("\""); - } - Args = builder.ToString(); - } - public Reset(string repo, string revision, string mode) { WorkingDirectory = repo; diff --git a/src/Commands/Restore.cs b/src/Commands/Restore.cs index 7a363543..663ea975 100644 --- a/src/Commands/Restore.cs +++ b/src/Commands/Restore.cs @@ -1,29 +1,52 @@ -using System.Collections.Generic; -using System.Text; +using System.Text; namespace SourceGit.Commands { public class Restore : Command { - public Restore(string repo) + /// + /// Only used for single staged change. + /// + /// + /// + public Restore(string repo, Models.Change stagedChange) { WorkingDirectory = repo; Context = repo; - Args = "restore . --source=HEAD --staged --worktree --recurse-submodules"; + + var builder = new StringBuilder(); + builder.Append("restore --staged -- \""); + builder.Append(stagedChange.Path); + builder.Append('"'); + + if (stagedChange.Index == Models.ChangeState.Renamed) + { + builder.Append(" \""); + builder.Append(stagedChange.OriginalPath); + builder.Append('"'); + } + + Args = builder.ToString(); } - public Restore(string repo, List files, string extra) + /// + /// Restore changes given in a path-spec file. + /// + /// + /// + /// + public Restore(string repo, string pathspecFile, bool isStaged) { WorkingDirectory = repo; Context = repo; - StringBuilder builder = new StringBuilder(); + var builder = new StringBuilder(); builder.Append("restore "); - if (!string.IsNullOrEmpty(extra)) - builder.Append(extra).Append(" "); - builder.Append("--"); - foreach (var f in files) - builder.Append(' ').Append('"').Append(f).Append('"'); + builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules "); + builder.Append("--pathspec-from-file=\""); + builder.Append(pathspecFile); + builder.Append('"'); + Args = builder.ToString(); } } diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 99e89093..64e8f8a5 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -10,15 +10,15 @@ namespace SourceGit.Commands { public static void Run(string repo, string revision, string file, string saveTo) { + var dir = Path.GetDirectoryName(saveTo); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result(); if (isLFSFiltered) { - var tmpFile = saveTo + ".tmp"; - if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) - { - ExecCmd(repo, $"lfs smudge", saveTo, tmpFile); - } - File.Delete(tmpFile); + var pointerStream = QueryFileContent.Run(repo, revision, file); + ExecCmd(repo, $"lfs smudge", saveTo, pointerStream); } else { @@ -26,7 +26,7 @@ namespace SourceGit.Commands } } - private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) + private static void ExecCmd(string repo, string args, string outputFile, Stream input = null) { var starter = new ProcessStartInfo(); starter.WorkingDirectory = repo; @@ -45,27 +45,11 @@ namespace SourceGit.Commands { var proc = new Process() { StartInfo = starter }; proc.Start(); - - if (inputFile != null) - { - using (StreamReader sr = new StreamReader(inputFile)) - { - while (true) - { - var line = sr.ReadLine(); - if (line == null) - break; - proc.StandardInput.WriteLine(line); - } - } - } - + if (input != null) + proc.StandardInput.Write(new StreamReader(input).ReadToEnd()); proc.StandardOutput.BaseStream.CopyTo(sw); proc.WaitForExit(); - var rs = proc.ExitCode == 0; proc.Close(); - - return rs; } catch (Exception e) { @@ -73,7 +57,6 @@ namespace SourceGit.Commands { App.RaiseException(repo, "Save file failed: " + e.Message); }); - return false; } } } diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 41b3889e..e11c1740 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace SourceGit.Commands { @@ -40,7 +40,7 @@ namespace SourceGit.Commands if (dateEndIdx == -1) return; - var dateStr = line.Substring(0, dateEndIdx); + var dateStr = line.AsSpan(0, dateEndIdx); if (double.TryParse(dateStr, out var date)) statistics.AddCommit(line.Substring(dateEndIdx + 1), date); } diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs index 9a273703..025d035a 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -1,4 +1,5 @@ -using System; +using System.Collections.Generic; +using System.Text; namespace SourceGit.Commands { @@ -10,10 +11,9 @@ namespace SourceGit.Commands Context = repo; } - public bool Add(string url, string relativePath, bool recursive, Action outputHandler) + public bool Add(string url, string relativePath, bool recursive) { - _outputHandler = outputHandler; - Args = $"submodule add {url} \"{relativePath}\""; + Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\""; if (!Exec()) return false; @@ -29,38 +29,38 @@ namespace SourceGit.Commands } } - public bool Update(string module, bool init, bool recursive, bool useRemote, Action outputHandler) + public bool Update(List modules, bool init, bool recursive, bool useRemote = false) { - Args = "submodule update"; + var builder = new StringBuilder(); + builder.Append("submodule update"); if (init) - Args += " --init"; + builder.Append(" --init"); if (recursive) - Args += " --recursive"; + builder.Append(" --recursive"); if (useRemote) - Args += " --remote"; - if (!string.IsNullOrEmpty(module)) - Args += $" -- \"{module}\""; + builder.Append(" --remote"); + if (modules.Count > 0) + { + builder.Append(" --"); + foreach (var module in modules) + builder.Append($" \"{module}\""); + } - _outputHandler = outputHandler; + Args = builder.ToString(); return Exec(); } - public bool Delete(string relativePath) + public bool Deinit(string module, bool force) { - Args = $"submodule deinit -f \"{relativePath}\""; - if (!Exec()) - return false; - - Args = $"rm -rf \"{relativePath}\""; + Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\""; return Exec(); } - protected override void OnReadline(string line) + public bool Delete(string module) { - _outputHandler?.Invoke(line); + Args = $"rm -rf \"{module}\""; + return Exec(); } - - private Action _outputHandler; } } diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index 23dbb11c..017afea0 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -1,57 +1,51 @@ -using System.Collections.Generic; -using System.IO; +using System.IO; namespace SourceGit.Commands { public static class Tag { - public static bool Add(string repo, string name, string basedOn) + public static bool Add(string repo, string name, string basedOn, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"tag {name} {basedOn}"; + cmd.Args = $"tag --no-sign {name} {basedOn}"; + cmd.Log = log; return cmd.Exec(); } - public static bool Add(string repo, string name, string basedOn, string message, bool sign) + public static bool Add(string repo, string name, string basedOn, string message, bool sign, Models.ICommandLog log) { var param = sign ? "--sign -a" : "--no-sign -a"; var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"tag {param} {name} {basedOn} "; + cmd.Log = log; if (!string.IsNullOrEmpty(message)) { string tmp = Path.GetTempFileName(); File.WriteAllText(tmp, message); cmd.Args += $"-F \"{tmp}\""; - } - else - { - cmd.Args += $"-m {name}"; + + var succ = cmd.Exec(); + File.Delete(tmp); + return succ; } + cmd.Args += $"-m {name}"; return cmd.Exec(); } - public static bool Delete(string repo, string name, List remotes) + public static bool Delete(string repo, string name, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"tag --delete {name}"; - if (!cmd.Exec()) - return false; - - if (remotes != null) - { - foreach (var r in remotes) - new Push(repo, r.Name, $"refs/tags/{name}", true).Exec(); - } - - return true; + cmd.Log = log; + return cmd.Exec(); } } } diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UnstageChangesForAmend.cs index c930f136..19def067 100644 --- a/src/Commands/UnstageChangesForAmend.cs +++ b/src/Commands/UnstageChangesForAmend.cs @@ -23,13 +23,11 @@ namespace SourceGit.Commands _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.OriginalPath); - _patchBuilder.Append("\n"); } else if (c.Index == Models.ChangeState.Added) { _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\n"); } else if (c.Index == Models.ChangeState.Deleted) { @@ -37,7 +35,6 @@ namespace SourceGit.Commands _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\n"); } else { @@ -46,8 +43,9 @@ namespace SourceGit.Commands _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\n"); } + + _patchBuilder.Append("\n"); } } diff --git a/src/Commands/UpdateRef.cs b/src/Commands/UpdateRef.cs deleted file mode 100644 index ba1b3d2f..00000000 --- a/src/Commands/UpdateRef.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace SourceGit.Commands -{ - public class UpdateRef : Command - { - public UpdateRef(string repo, string refName, string toRevision, Action outputHandler) - { - _outputHandler = outputHandler; - - WorkingDirectory = repo; - Context = repo; - Args = $"update-ref {refName} {toRevision}"; - } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private Action _outputHandler; - } -} diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index 960d5501..1198a443 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -56,7 +56,7 @@ namespace SourceGit.Commands return worktrees; } - public bool Add(string fullpath, string name, bool createNew, string tracking, Action outputHandler) + public bool Add(string fullpath, string name, bool createNew, string tracking) { Args = "worktree add "; @@ -78,14 +78,12 @@ namespace SourceGit.Commands else if (!string.IsNullOrEmpty(name) && !createNew) Args += name; - _outputHandler = outputHandler; return Exec(); } - public bool Prune(Action outputHandler) + public bool Prune() { Args = "worktree prune -v"; - _outputHandler = outputHandler; return Exec(); } @@ -101,22 +99,14 @@ namespace SourceGit.Commands return Exec(); } - public bool Remove(string fullpath, bool force, Action outputHandler) + public bool Remove(string fullpath, bool force) { if (force) Args = $"worktree remove -f \"{fullpath}\""; else Args = $"worktree remove \"{fullpath}\""; - _outputHandler = outputHandler; return Exec(); } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private Action _outputHandler = null; } } diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index 81cac8b7..6f3ae98b 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -7,8 +7,11 @@ namespace SourceGit.Converters { public static class ListConverters { + public static readonly FuncValueConverter Count = + new FuncValueConverter(v => v == null ? "0" : $"{v.Count}"); + public static readonly FuncValueConverter ToCount = - new FuncValueConverter(v => v == null ? " (0)" : $" ({v.Count})"); + new FuncValueConverter(v => v == null ? "(0)" : $"({v.Count})"); public static readonly FuncValueConverter IsNullOrEmpty = new FuncValueConverter(v => v == null || v.Count == 0); diff --git a/src/Converters/ObjectConverters.cs b/src/Converters/ObjectConverters.cs new file mode 100644 index 00000000..f7c57764 --- /dev/null +++ b/src/Converters/ObjectConverters.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class ObjectConverters + { + public class IsTypeOfConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null || parameter == null) + return false; + + return value.GetType().IsAssignableTo((Type)parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return new NotImplementedException(); + } + } + + public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter(); + } +} diff --git a/src/Converters/PathConverters.cs b/src/Converters/PathConverters.cs index dd7cfa49..ac1e61e5 100644 --- a/src/Converters/PathConverters.cs +++ b/src/Converters/PathConverters.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Avalonia.Data.Converters; @@ -22,7 +22,7 @@ namespace SourceGit.Converters var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; if (v.StartsWith(home, StringComparison.Ordinal)) - return "~" + v.Substring(prefixLen); + return $"~{v.AsSpan(prefixLen)}"; return v; }); diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index a506d886..bc9fbe95 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -17,7 +17,7 @@ namespace SourceGit.Models { public interface IAvatarHost { - void OnAvatarResourceChanged(string email); + void OnAvatarResourceChanged(string email, Bitmap image); } public partial class AvatarManager @@ -26,10 +26,7 @@ namespace SourceGit.Models { get { - if (_instance == null) - _instance = new AvatarManager(); - - return _instance; + return _instance ??= new AvatarManager(); } } @@ -38,7 +35,7 @@ namespace SourceGit.Models [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")] private static partial Regex REG_GITHUB_USER_EMAIL(); - private object _synclock = new object(); + private readonly Lock _synclock = new(); private string _storePath; private List _avatars = new List(); private Dictionary _resources = new Dictionary(); @@ -119,7 +116,7 @@ namespace SourceGit.Models Dispatcher.UIThread.InvokeAsync(() => { _resources[email] = img; - NotifyResourceChanged(email); + NotifyResourceChanged(email, img); }); } @@ -144,14 +141,13 @@ namespace SourceGit.Models if (_defaultAvatars.Contains(email)) return null; - if (_resources.ContainsKey(email)) - _resources.Remove(email); + _resources.Remove(email); var localFile = Path.Combine(_storePath, GetEmailHash(email)); if (File.Exists(localFile)) File.Delete(localFile); - NotifyResourceChanged(email); + NotifyResourceChanged(email, null); } else { @@ -179,13 +175,40 @@ namespace SourceGit.Models lock (_synclock) { - if (!_requesting.Contains(email)) - _requesting.Add(email); + _requesting.Add(email); } return null; } + public void SetFromLocal(string email, string file) + { + try + { + Bitmap image = null; + + using (var stream = File.OpenRead(file)) + { + image = Bitmap.DecodeToWidth(stream, 128); + } + + if (image == null) + return; + + _resources[email] = image; + + _requesting.Remove(email); + + var store = Path.Combine(_storePath, GetEmailHash(email)); + File.Copy(file, store, true); + NotifyResourceChanged(email, image); + } + catch + { + // ignore + } + } + private void LoadDefaultAvatar(string key, string img) { var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute)); @@ -196,19 +219,17 @@ namespace SourceGit.Models private string GetEmailHash(string email) { var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim(); - var hash = MD5.HashData(Encoding.Default.GetBytes(lowered).AsSpan()); + var hash = MD5.HashData(Encoding.Default.GetBytes(lowered)); var builder = new StringBuilder(hash.Length * 2); foreach (var c in hash) builder.Append(c.ToString("x2")); return builder.ToString(); } - private void NotifyResourceChanged(string email) + private void NotifyResourceChanged(string email, Bitmap image) { foreach (var avatar in _avatars) - { - avatar.OnAvatarResourceChanged(email); - } + avatar.OnAvatarResourceChanged(email, image); } } } diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs new file mode 100644 index 00000000..2ed8beb2 --- /dev/null +++ b/src/Models/Bisect.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public enum BisectState + { + None = 0, + WaitingForRange, + Detecting, + } + + [Flags] + public enum BisectCommitFlag + { + None = 0, + Good = 1 << 0, + Bad = 1 << 1, + } + + public class Bisect + { + public HashSet Bads + { + get; + set; + } = []; + + public HashSet Goods + { + get; + set; + } = []; + } +} diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index 2d0ae5b2..7146da3f 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -23,10 +23,17 @@ namespace SourceGit.Models } } + public enum BranchSortMode + { + Name = 0, + CommitterDate, + } + public class Branch { public string Name { get; set; } public string FullName { get; set; } + public ulong CommitterDate { get; set; } public string Head { get; set; } public bool IsLocal { get; set; } public bool IsCurrent { get; set; } @@ -34,7 +41,7 @@ namespace SourceGit.Models public string Upstream { get; set; } public BranchTrackStatus TrackStatus { get; set; } public string Remote { get; set; } - public bool IsUpsteamGone { get; set; } + public bool IsUpstreamGone { get; set; } public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; } diff --git a/src/Models/Change.cs b/src/Models/Change.cs index e9d07181..129678be 100644 --- a/src/Models/Change.cs +++ b/src/Models/Change.cs @@ -18,14 +18,27 @@ namespace SourceGit.Models Deleted, Renamed, Copied, - Unmerged, - Untracked + Untracked, + Conflicted, + } + + public enum ConflictReason + { + None, + BothDeleted, + AddedByUs, + DeletedByThem, + AddedByThem, + DeletedByUs, + BothAdded, + BothModified, } public class ChangeDataForAmend { public string FileMode { get; set; } = ""; public string ObjectHash { get; set; } = ""; + public string ParentSHA { get; set; } = ""; } public class Change @@ -35,20 +48,14 @@ namespace SourceGit.Models public string Path { get; set; } = ""; public string OriginalPath { get; set; } = ""; public ChangeDataForAmend DataForAmend { get; set; } = null; + public ConflictReason ConflictReason { get; set; } = ConflictReason.None; - public bool IsConflict - { - get - { - if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged) - return true; - if (Index == ChangeState.Added && WorkTree == ChangeState.Added) - return true; - if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted) - return true; - return false; - } - } + public bool IsConflicted => WorkTree == ChangeState.Conflicted; + public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason]; + public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason]; + + public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree]; + public string IndexDesc => TYPE_DESCS[(int)Index]; public void Set(ChangeState index, ChangeState workTree = ChangeState.None) { @@ -76,8 +83,44 @@ namespace SourceGit.Models if (Path[0] == '"') Path = Path.Substring(1, Path.Length - 2); + if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2); } + + private static readonly string[] TYPE_DESCS = + [ + "Unknown", + "Modified", + "Type Changed", + "Added", + "Deleted", + "Renamed", + "Copied", + "Untracked", + "Conflict" + ]; + private static readonly string[] CONFLICT_MARKERS = + [ + string.Empty, + "DD", + "AU", + "UD", + "UA", + "DU", + "AA", + "UU" + ]; + private static readonly string[] CONFLICT_DESCS = + [ + string.Empty, + "Both deleted", + "Added by us", + "Deleted by them", + "Added by them", + "Deleted by us", + "Both added", + "Both modified" + ]; } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 0bad8376..f0f4b39b 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -13,10 +13,14 @@ namespace SourceGit.Models ByCommitter, ByMessage, ByFile, + ByContent, } public class Commit { + // As retrieved by: git mktree Parents { get; set; } = new List(); - public List Decorators { get; set; } = new List(); + public List Parents { get; set; } = new(); + public List Decorators { get; set; } = new(); public bool HasDecorators => Decorators.Count > 0; - public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime); - public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime); - public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly); - public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly); + public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); + public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); + public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly); + public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly); public bool IsMerged { get; set; } = false; public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; @@ -45,7 +49,7 @@ namespace SourceGit.Models public int Color { get; set; } = 0; public double Opacity => IsMerged ? 1 : OpacityForNotMerged; public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular; - public Thickness Margin { get; set; } = new Thickness(0); + public Thickness Margin { get; set; } = new(0); public IBrush Brush => CommitGraph.Pens[Color].Brush; public void ParseDecorators(string data) @@ -109,7 +113,7 @@ namespace SourceGit.Models if (l.Type != r.Type) return (int)l.Type - (int)r.Type; else - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); + return NumericSort.Compare(l.Name, r.Name); }); } } @@ -117,6 +121,6 @@ namespace SourceGit.Models public class CommitFullMessage { public string Message { get; set; } = string.Empty; - public List Links { get; set; } = []; + public InlineElementCollector Inlines { get; set; } = new(); } } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 77209751..01488656 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -64,8 +64,8 @@ namespace SourceGit.Models { const double unitWidth = 12; const double halfWidth = 6; - const double unitHeight = 28; - const double halfHeight = 14; + const double unitHeight = 1; + const double halfHeight = 0.5; var temp = new CommitGraph(); var unsolved = new List(); diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs index 955779a8..2891e5d6 100644 --- a/src/Models/CommitLink.cs +++ b/src/Models/CommitLink.cs @@ -1,8 +1,49 @@ -namespace SourceGit.Models +using System; +using System.Collections.Generic; + +namespace SourceGit.Models { public class CommitLink { public string Name { get; set; } = null; public string URLPrefix { get; set; } = null; + + public CommitLink(string name, string prefix) + { + Name = name; + URLPrefix = prefix; + } + + public static List Get(List remotes) + { + var outs = new List(); + + foreach (var remote in remotes) + { + if (remote.TryGetVisitURL(out var url)) + { + var trimmedUrl = url.AsSpan(); + if (url.EndsWith(".git")) + trimmedUrl = url.AsSpan(0, url.Length - 4); + + if (url.StartsWith("https://github.com/", StringComparison.Ordinal)) + outs.Add(new($"Github ({trimmedUrl.Slice(19)})", $"{url}/commit/")); + else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal)) + outs.Add(new($"GitLab ({trimmedUrl.Slice(trimmedUrl.Slice(15).IndexOf('/') + 16)})", $"{url}/-/commit/")); + else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal)) + outs.Add(new($"Gitee ({trimmedUrl.Slice(18)})", $"{url}/commit/")); + else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal)) + outs.Add(new($"BitBucket ({trimmedUrl.Slice(22)})", $"{url}/commits/")); + else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal)) + outs.Add(new($"Codeberg ({trimmedUrl.Slice(21)})", $"{url}/commit/")); + else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal)) + outs.Add(new($"Gitea ({trimmedUrl.Slice(18)})", $"{url}/commit/")); + else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal)) + outs.Add(new($"sourcehut ({trimmedUrl.Slice(18)})", $"{url}/commit/")); + } + } + + return outs; + } } } diff --git a/src/Models/CommitTemplate.cs b/src/Models/CommitTemplate.cs index 56e1992c..3f331543 100644 --- a/src/Models/CommitTemplate.cs +++ b/src/Models/CommitTemplate.cs @@ -4,7 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { - public partial class CommitTemplate : ObservableObject + public class CommitTemplate : ObservableObject { public string Name { diff --git a/src/Models/ConventionalCommitType.cs b/src/Models/ConventionalCommitType.cs index 4fb61d87..531a16c0 100644 --- a/src/Models/ConventionalCommitType.cs +++ b/src/Models/ConventionalCommitType.cs @@ -4,23 +4,28 @@ namespace SourceGit.Models { public class ConventionalCommitType { - public string Type { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; + public string Name { get; set; } + public string Type { get; set; } + public string Description { get; set; } - public static readonly List Supported = new List() - { - new ConventionalCommitType("feat", "Adding a new feature"), - new ConventionalCommitType("fix", "Fixing a bug"), - new ConventionalCommitType("docs", "Updating documentation"), - new ConventionalCommitType("style", "Elements or code styles without changing the code logic"), - new ConventionalCommitType("test", "Adding or updating tests"), - new ConventionalCommitType("chore", "Making changes to the build process or auxiliary tools and libraries"), - new ConventionalCommitType("revert", "Undoing a previous commit"), - new ConventionalCommitType("refactor", "Restructuring code without changing its external behavior") - }; + public static readonly List Supported = [ + new("Features", "feat", "Adding a new feature"), + new("Bug Fixes", "fix", "Fixing a bug"), + new("Work In Progress", "wip", "Still being developed and not yet complete"), + new("Reverts", "revert", "Undoing a previous commit"), + new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"), + new("Performance Improvements", "perf", "Improves performance"), + new("Builds", "build", "Changes that affect the build system or external dependencies"), + new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"), + new("Documentations", "docs", "Updating documentation"), + new("Styles", "style", "Elements or code styles without changing the code logic"), + new("Tests", "test", "Adding or updating tests"), + new("Chores", "chore", "Other changes that don't modify src or test files"), + ]; - public ConventionalCommitType(string type, string description) + public ConventionalCommitType(string name, string type, string description) { + Name = name; Type = type; Description = description; } diff --git a/src/Models/Count.cs b/src/Models/Count.cs new file mode 100644 index 00000000..d48b0c08 --- /dev/null +++ b/src/Models/Count.cs @@ -0,0 +1,19 @@ +using System; + +namespace SourceGit.Models +{ + public class Count : IDisposable + { + public int Value { get; set; } = 0; + + public Count(int value) + { + Value = value; + } + + public void Dispose() + { + // Ignore + } + } +} diff --git a/src/Models/DateTimeFormat.cs b/src/Models/DateTimeFormat.cs index 4e8aa550..16276c40 100644 --- a/src/Models/DateTimeFormat.cs +++ b/src/Models/DateTimeFormat.cs @@ -25,7 +25,7 @@ namespace SourceGit.Models set; } = 0; - public static DateTimeFormat Actived + public static DateTimeFormat Active { get => Supported[ActiveIndex]; } diff --git a/src/Models/DealWithChangesAfterStashing.cs b/src/Models/DealWithChangesAfterStashing.cs new file mode 100644 index 00000000..63889c96 --- /dev/null +++ b/src/Models/DealWithChangesAfterStashing.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class DealWithChangesAfterStashing + { + public string Label { get; set; } + public string Desc { get; set; } + + public static readonly List Supported = [ + new ("Discard", "All (or selected) changes will be discarded"), + new ("Keep Index", "Staged changes are left intact"), + new ("Keep All", "All (or selected) changes are left intact"), + ]; + + public DealWithChangesAfterStashing(string label, string desc) + { + Label = label; + Desc = desc; + } + } +} diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index 98387e7f..69f93980 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -5,6 +5,15 @@ namespace SourceGit.Models { public class DiffOption { + /// + /// Enable `--ignore-cr-at-eol` by default? + /// + public static bool IgnoreCRAtEOL + { + get; + set; + } = true; + public Change WorkingCopyChange => _workingCopyChange; public bool IsUnstaged => _isUnstaged; public List Revisions => _revisions; @@ -40,7 +49,7 @@ namespace SourceGit.Models else { if (change.DataForAmend != null) - _extra = "--cached HEAD^"; + _extra = $"--cached {change.DataForAmend.ParentSHA}"; else _extra = "--cached"; @@ -56,7 +65,7 @@ namespace SourceGit.Models /// public DiffOption(Commit commit, Change change) { - var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^"; + var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; _revisions.Add(baseRevision); _revisions.Add(commit.SHA); _path = change.Path; @@ -70,7 +79,7 @@ namespace SourceGit.Models /// public DiffOption(Commit commit, string file) { - var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^"; + var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; _revisions.Add(baseRevision); _revisions.Add(commit.SHA); _path = file; @@ -115,6 +124,6 @@ namespace SourceGit.Models private readonly string _path; private readonly string _orgPath = string.Empty; private readonly string _extra = string.Empty; - private readonly List _revisions = new List(); + private readonly List _revisions = []; } } diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 88992e10..71ed2d9e 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -16,11 +16,10 @@ namespace SourceGit.Models Deleted, } - public class TextInlineRange + public class TextInlineRange(int p, int n) { - public int Start { get; set; } - public int End { get; set; } - public TextInlineRange(int p, int n) { Start = p; End = p + n - 1; } + public int Start { get; set; } = p; + public int End { get; set; } = p + n - 1; } public class TextDiffLine @@ -30,6 +29,7 @@ namespace SourceGit.Models public int OldLineNumber { get; set; } = 0; public int NewLineNumber { get; set; } = 0; public List Highlights { get; set; } = new List(); + public bool NoNewLineEndOfFile { get; set; } = false; public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString(); public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); @@ -146,7 +146,7 @@ namespace SourceGit.Models public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) { var isTracked = !string.IsNullOrEmpty(fileBlobGuid); - var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000"; + var fileGuid = isTracked ? fileBlobGuid : "00000000"; var builder = new StringBuilder(); builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n'); @@ -555,7 +555,7 @@ namespace SourceGit.Models } else if (test.Type == TextDiffLineType.Added) { - if (i < start - 1) + if (i < start - 1 || isOldSide) { if (revert) { @@ -565,18 +565,7 @@ namespace SourceGit.Models } else { - if (isOldSide) - { - if (revert) - { - newCount++; - oldCount++; - } - } - else - { - newCount++; - } + newCount++; } if (i == end - 1 && tailed) @@ -654,9 +643,7 @@ namespace SourceGit.Models public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0"; } - public class NoOrEOLChange - { - } + public class NoOrEOLChange; public class FileModeDiff { diff --git a/src/Models/DirtyState.cs b/src/Models/DirtyState.cs new file mode 100644 index 00000000..2b9d898d --- /dev/null +++ b/src/Models/DirtyState.cs @@ -0,0 +1,12 @@ +using System; + +namespace SourceGit.Models +{ + [Flags] + public enum DirtyState + { + None = 0, + HasLocalChanges = 1 << 0, + HasPendingPullOrPush = 1 << 1, + } +} diff --git a/src/Models/ExternalMerger.cs b/src/Models/ExternalMerger.cs index d97d3933..fe67ad6a 100644 --- a/src/Models/ExternalMerger.cs +++ b/src/Models/ExternalMerger.cs @@ -43,6 +43,7 @@ namespace SourceGit.Models new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(10, "plastic_merge", "Plastic SCM", "mergetool.exe", "-s=\"$REMOTE\" -b=\"$BASE\" -d=\"$LOCAL\" -r=\"$MERGED\" --automatic", "-s=\"$LOCAL\" -d=\"$REMOTE\""), + new ExternalMerger(11, "meld", "Meld", "Meld.exe", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), }; } else if (OperatingSystem.IsMacOS()) diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index 103e91bc..697c171a 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -107,8 +107,7 @@ namespace SourceGit.Models // Ignore } - if (_customPaths == null) - _customPaths = new ExternalToolPaths(); + _customPaths ??= new ExternalToolPaths(); } public void TryAdd(string name, string icon, Func finder, Func execArgsGenerator = null) diff --git a/src/Models/GitFlow.cs b/src/Models/GitFlow.cs new file mode 100644 index 00000000..5d26072b --- /dev/null +++ b/src/Models/GitFlow.cs @@ -0,0 +1,46 @@ +namespace SourceGit.Models +{ + public enum GitFlowBranchType + { + None = 0, + Feature, + Release, + Hotfix, + } + + public class GitFlow + { + public string Master { get; set; } = string.Empty; + public string Develop { get; set; } = string.Empty; + public string FeaturePrefix { get; set; } = string.Empty; + public string ReleasePrefix { get; set; } = string.Empty; + public string HotfixPrefix { get; set; } = string.Empty; + + public bool IsValid + { + get + { + return !string.IsNullOrEmpty(Master) && + !string.IsNullOrEmpty(Develop) && + !string.IsNullOrEmpty(FeaturePrefix) && + !string.IsNullOrEmpty(ReleasePrefix) && + !string.IsNullOrEmpty(HotfixPrefix); + } + } + + public string GetPrefix(GitFlowBranchType type) + { + switch (type) + { + case GitFlowBranchType.Feature: + return FeaturePrefix; + case GitFlowBranchType.Release: + return ReleasePrefix; + case GitFlowBranchType.Hotfix: + return HotfixPrefix; + default: + return string.Empty; + } + } + } +} diff --git a/src/Models/GitVersions.cs b/src/Models/GitVersions.cs index 92fd8c59..8aae63a3 100644 --- a/src/Models/GitVersions.cs +++ b/src/Models/GitVersions.cs @@ -5,26 +5,16 @@ /// /// The minimal version of Git that required by this app. /// - public static readonly System.Version MINIMAL = new System.Version(2, 23, 0); - - /// - /// The minimal version of Git that supports the `add` command with the `--pathspec-from-file` option. - /// - public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0); + public static readonly System.Version MINIMAL = new(2, 25, 1); /// /// The minimal version of Git that supports the `stash push` command with the `--pathspec-from-file` option. /// - public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new System.Version(2, 26, 0); + public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new(2, 26, 0); /// /// The minimal version of Git that supports the `stash push` command with the `--staged` option. /// - public static readonly System.Version STASH_PUSH_ONLY_STAGED = new System.Version(2, 35, 0); - - /// - /// The minimal version of Git that supports the `stash show` command with the `-u` option. - /// - public static readonly System.Version STASH_SHOW_WITH_UNTRACKED = new System.Version(2, 32, 0); + public static readonly System.Version STASH_PUSH_ONLY_STAGED = new(2, 35, 0); } } diff --git a/src/Models/Hyperlink.cs b/src/Models/Hyperlink.cs deleted file mode 100644 index 81dc980e..00000000 --- a/src/Models/Hyperlink.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace SourceGit.Models -{ - public class Hyperlink - { - public int Start { get; set; } = 0; - public int Length { get; set; } = 0; - public string Link { get; set; } = ""; - public bool IsCommitSHA { get; set; } = false; - - public Hyperlink(int start, int length, string link, bool isCommitSHA = false) - { - Start = start; - Length = length; - Link = link; - IsCommitSHA = isCommitSHA; - } - - public bool Intersect(int start, int length) - { - if (start == Start) - return true; - - if (start < Start) - return start + length > Start; - - return start < Start + Length; - } - } -} diff --git a/src/Models/ICommandLog.cs b/src/Models/ICommandLog.cs new file mode 100644 index 00000000..34ec7031 --- /dev/null +++ b/src/Models/ICommandLog.cs @@ -0,0 +1,7 @@ +namespace SourceGit.Models +{ + public interface ICommandLog + { + void AppendLine(string line); + } +} diff --git a/src/Models/IRepository.cs b/src/Models/IRepository.cs index 0224d81f..2fc7c612 100644 --- a/src/Models/IRepository.cs +++ b/src/Models/IRepository.cs @@ -2,6 +2,8 @@ { public interface IRepository { + bool MayHaveSubmodules(); + void RefreshBranches(); void RefreshWorktrees(); void RefreshTags(); diff --git a/src/Models/ImageDecoder.cs b/src/Models/ImageDecoder.cs new file mode 100644 index 00000000..6fe0f428 --- /dev/null +++ b/src/Models/ImageDecoder.cs @@ -0,0 +1,10 @@ +namespace SourceGit.Models +{ + public enum ImageDecoder + { + None = 0, + Builtin, + Pfim, + Tiff, + } +} diff --git a/src/Models/InlineElement.cs b/src/Models/InlineElement.cs new file mode 100644 index 00000000..ea7bcee8 --- /dev/null +++ b/src/Models/InlineElement.cs @@ -0,0 +1,37 @@ +namespace SourceGit.Models +{ + public enum InlineElementType + { + Keyword = 0, + Link, + CommitSHA, + Code, + } + + public class InlineElement + { + public InlineElementType Type { get; } + public int Start { get; } + public int Length { get; } + public string Link { get; } + + public InlineElement(InlineElementType type, int start, int length, string link) + { + Type = type; + Start = start; + Length = length; + Link = link; + } + + public bool IsIntersecting(int start, int length) + { + if (start == Start) + return true; + + if (start < Start) + return start + length > Start; + + return start < Start + Length; + } + } +} diff --git a/src/Models/InlineElementCollector.cs b/src/Models/InlineElementCollector.cs new file mode 100644 index 00000000..d81aaf8d --- /dev/null +++ b/src/Models/InlineElementCollector.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class InlineElementCollector + { + public int Count => _implementation.Count; + public InlineElement this[int index] => _implementation[index]; + + public InlineElement Intersect(int start, int length) + { + foreach (var elem in _implementation) + { + if (elem.IsIntersecting(start, length)) + return elem; + } + + return null; + } + + public void Add(InlineElement element) + { + _implementation.Add(element); + } + + public void Sort() + { + _implementation.Sort((l, r) => l.Start.CompareTo(r.Start)); + } + + public void Clear() + { + _implementation.Clear(); + } + + private readonly List _implementation = []; + } +} diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs index 691aadeb..d1710d4a 100644 --- a/src/Models/InteractiveRebase.cs +++ b/src/Models/InteractiveRebase.cs @@ -27,6 +27,8 @@ namespace SourceGit.Models public class InteractiveRebaseJobCollection { + public string OrigHead { get; set; } = string.Empty; + public string Onto { get; set; } = string.Empty; public List Jobs { get; set; } = new List(); } } diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs new file mode 100644 index 00000000..001c65a6 --- /dev/null +++ b/src/Models/IpcChannel.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace SourceGit.Models +{ + public class IpcChannel : IDisposable + { + public bool IsFirstInstance { get; } + + public event Action MessageReceived; + + public IpcChannel() + { + try + { + _singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + IsFirstInstance = true; + _server = new NamedPipeServerStream( + "SourceGitIPCChannel" + Environment.UserName, + PipeDirection.In, + -1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + _cancellationTokenSource = new CancellationTokenSource(); + Task.Run(StartServer); + } + catch + { + IsFirstInstance = false; + } + } + + public void SendToFirstInstance(string cmd) + { + try + { + using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) + { + client.Connect(1000); + if (!client.IsConnected) + return; + + using (var writer = new StreamWriter(client)) + { + writer.WriteLine(cmd); + writer.Flush(); + } + + if (OperatingSystem.IsWindows()) + client.WaitForPipeDrain(); + else + Thread.Sleep(1000); + } + } + catch + { + // IGNORE + } + } + + public void Dispose() + { + _cancellationTokenSource?.Cancel(); + _singletonLock?.Dispose(); + } + + private async void StartServer() + { + using var reader = new StreamReader(_server); + + while (!_cancellationTokenSource.IsCancellationRequested) + { + try + { + await _server.WaitForConnectionAsync(_cancellationTokenSource.Token); + + if (!_cancellationTokenSource.IsCancellationRequested) + { + var line = await reader.ReadToEndAsync(_cancellationTokenSource.Token); + MessageReceived?.Invoke(line?.Trim()); + } + + _server.Disconnect(); + } + catch + { + if (!_cancellationTokenSource.IsCancellationRequested && _server.IsConnected) + _server.Disconnect(); + } + } + } + + private FileStream _singletonLock = null; + private NamedPipeServerStream _server = null; + private CancellationTokenSource _cancellationTokenSource = null; + } +} diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs index 29487a16..40c84b9e 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTrackerRule.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using CommunityToolkit.Mvvm.ComponentModel; @@ -46,7 +45,7 @@ namespace SourceGit.Models set => SetProperty(ref _urlTemplate, value); } - public void Matches(List outs, string message) + public void Matches(InlineElementCollector outs, string message) { if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) return; @@ -60,17 +59,7 @@ namespace SourceGit.Models var start = match.Index; var len = match.Length; - var intersect = false; - foreach (var exist in outs) - { - if (exist.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (intersect) + if (outs.Intersect(start, len) != null) continue; var link = _urlTemplate; @@ -81,8 +70,7 @@ namespace SourceGit.Models link = link.Replace($"${j}", group.Value); } - var range = new Hyperlink(start, len, link); - outs.Add(range); + outs.Add(new InlineElement(InlineElementType.Link, start, len, link)); } } diff --git a/src/Models/LFSObject.cs b/src/Models/LFSObject.cs index 0f281253..8bc2dda2 100644 --- a/src/Models/LFSObject.cs +++ b/src/Models/LFSObject.cs @@ -1,8 +1,22 @@ -namespace SourceGit.Models +using System.Text.RegularExpressions; + +namespace SourceGit.Models { - public class LFSObject + public partial class LFSObject { + [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] + private static partial Regex REG_FORMAT(); + public string Oid { get; set; } = string.Empty; public long Size { get; set; } = 0; + + public static LFSObject Parse(string content) + { + var match = REG_FORMAT().Match(content); + if (match.Success) + return new() { Oid = match.Groups[1].Value, Size = long.Parse(match.Groups[2].Value) }; + + return null; + } } } diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index a7884e8b..1788a9b2 100644 --- a/src/Models/Locales.cs +++ b/src/Models/Locales.cs @@ -14,6 +14,7 @@ namespace SourceGit.Models new Locale("Français", "fr_FR"), new Locale("Italiano", "it_IT"), new Locale("Português (Brasil)", "pt_BR"), + new Locale("Українська", "uk_UA"), new Locale("Русский", "ru_RU"), new Locale("简体中文", "zh_CN"), new Locale("繁體中文", "zh_TW"), diff --git a/src/Models/Null.cs b/src/Models/Null.cs index e22ef8b3..1820d4d0 100644 --- a/src/Models/Null.cs +++ b/src/Models/Null.cs @@ -1,6 +1,4 @@ namespace SourceGit.Models { - public class Null - { - } + public class Null; } diff --git a/src/Models/NumericSort.cs b/src/Models/NumericSort.cs index ed5002e6..baaf3da4 100644 --- a/src/Models/NumericSort.cs +++ b/src/Models/NumericSort.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Models +using System; + +namespace SourceGit.Models { public static class NumericSort { @@ -10,52 +12,35 @@ int marker1 = 0; int marker2 = 0; - char[] tmp1 = new char[len1]; - char[] tmp2 = new char[len2]; - while (marker1 < len1 && marker2 < len2) { char c1 = s1[marker1]; char c2 = s2[marker2]; - int loc1 = 0; - int loc2 = 0; bool isDigit1 = char.IsDigit(c1); bool isDigit2 = char.IsDigit(c2); if (isDigit1 != isDigit2) return c1.CompareTo(c2); - do - { - tmp1[loc1] = c1; - loc1++; - marker1++; + int subLen1 = 1; + while (marker1 + subLen1 < len1 && char.IsDigit(s1[marker1 + subLen1]) == isDigit1) + subLen1++; - if (marker1 < len1) - c1 = s1[marker1]; - else - break; - } while (char.IsDigit(c1) == isDigit1); + int subLen2 = 1; + while (marker2 + subLen2 < len2 && char.IsDigit(s2[marker2 + subLen2]) == isDigit2) + subLen2++; - do - { - tmp2[loc2] = c2; - loc2++; - marker2++; + string sub1 = s1.Substring(marker1, subLen1); + string sub2 = s2.Substring(marker2, subLen2); - if (marker2 < len2) - c2 = s2[marker2]; - else - break; - } while (char.IsDigit(c2) == isDigit2); + marker1 += subLen1; + marker2 += subLen2; - string sub1 = new string(tmp1, 0, loc1); - string sub2 = new string(tmp2, 0, loc2); int result; if (isDigit1) - result = loc1 == loc2 ? string.CompareOrdinal(sub1, sub2) : loc1 - loc2; + result = (subLen1 == subLen2) ? string.CompareOrdinal(sub1, sub2) : (subLen1 - subLen2); else - result = string.CompareOrdinal(sub1, sub2); + result = string.Compare(sub1, sub2, StringComparison.OrdinalIgnoreCase); if (result != 0) return result; diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index ec9b8f20..6e36cfb9 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -6,8 +6,10 @@ namespace SourceGit.Models { public partial class Remote { - [GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")] + [GeneratedRegex(@"^https?://[^/]+/.+[^/\.]$")] private static partial Regex REG_HTTPS(); + [GeneratedRegex(@"^git://[^/]+/.+[^/\.]$")] + private static partial Regex REG_GIT(); [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")] private static partial Regex REG_SSH1(); [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")] @@ -18,6 +20,7 @@ namespace SourceGit.Models private static readonly Regex[] URL_FORMATS = [ REG_HTTPS(), + REG_GIT(), REG_SSH1(), REG_SSH2(), ]; @@ -30,13 +33,10 @@ namespace SourceGit.Models if (string.IsNullOrWhiteSpace(url)) return false; - for (int i = 1; i < URL_FORMATS.Length; i++) - { - if (URL_FORMATS[i].IsMatch(url)) - return true; - } + if (REG_SSH1().IsMatch(url)) + return true; - return false; + return REG_SSH2().IsMatch(url); } public static bool IsValidURL(string url) @@ -50,7 +50,10 @@ namespace SourceGit.Models return true; } - return url.EndsWith(".git", StringComparison.Ordinal) && Directory.Exists(url); + return url.StartsWith("file://", StringComparison.Ordinal) || + url.StartsWith("./", StringComparison.Ordinal) || + url.StartsWith("../", StringComparison.Ordinal) || + Directory.Exists(url); } public bool TryGetVisitURL(out string url) diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index fc5e81b4..4e51b368 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -32,12 +32,24 @@ namespace SourceGit.Models set; } = false; - public bool OnlyHighlighCurrentBranchInHistories + public bool OnlyHighlightCurrentBranchInHistories { get; set; } = false; + public BranchSortMode LocalBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public BranchSortMode RemoteBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + public TagSortMode TagSortMode { get; @@ -68,18 +80,6 @@ namespace SourceGit.Models set; } = true; - public bool FetchWithoutTagsOnPull - { - get; - set; - } = false; - - public bool FetchAllBranchesOnPull - { - get; - set; - } = true; - public bool CheckSubmodulesOnPush { get; @@ -110,6 +110,12 @@ namespace SourceGit.Models set; } = true; + public bool UpdateSubmodulesOnCheckoutBranch + { + get; + set; + } = true; + public AvaloniaList HistoriesFilters { get; @@ -170,19 +176,13 @@ namespace SourceGit.Models set; } = false; - public bool KeepIndexWhenStash + public int ChangesAfterStashing { get; set; - } = false; + } = 0; - public bool AutoRestoreAfterStash - { - get; - set; - } = false; - - public string PreferedOpenAIService + public string PreferredOpenAIService { get; set; @@ -230,6 +230,12 @@ namespace SourceGit.Models set; } = 0; + public string LastCommitMessage + { + get; + set; + } = string.Empty; + public Dictionary CollectHistoriesFilters() { var map = new Dictionary(); @@ -275,9 +281,8 @@ namespace SourceGit.Models return false; } - for (int i = 0; i < HistoriesFilters.Count; i++) + foreach (var filter in HistoriesFilters) { - var filter = HistoriesFilters[i]; if (filter.Type != type) continue; @@ -320,28 +325,28 @@ namespace SourceGit.Models if (filter.Mode == FilterMode.Included) includedRefs.Add(filter.Pattern); else if (filter.Mode == FilterMode.Excluded) - excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); + 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.Substring(11)}/*"); + includedRefs.Add($"--branches={filter.Pattern.AsSpan(11)}/*"); else if (filter.Mode == FilterMode.Excluded) - excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); + 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.Substring(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); + 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.Substring(13)}/*"); + includedRefs.Add($"--remotes={filter.Pattern.AsSpan(13)}/*"); else if (filter.Mode == FilterMode.Excluded) - excludedRemotes.Add($"--exclude=\"{filter.Pattern.Substring(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); } else if (filter.Type == FilterType.Tag) { @@ -393,6 +398,7 @@ namespace SourceGit.Models public void PushCommitMessage(string message) { + message = message.Trim().ReplaceLineEndings("\n"); var existIdx = CommitMessages.IndexOf(message); if (existIdx == 0) return; @@ -440,5 +446,19 @@ namespace SourceGit.Models if (act != null) CustomActions.Remove(act); } + + public void MoveCustomActionUp(CustomAction act) + { + var idx = CustomActions.IndexOf(act); + if (idx > 0) + CustomActions.Move(idx - 1, idx); + } + + public void MoveCustomActionDown(CustomAction act) + { + var idx = CustomActions.IndexOf(act); + if (idx < CustomActions.Count - 1) + CustomActions.Move(idx + 1, idx); + } } } diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index 8cc1be2a..29a23efa 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -1,4 +1,6 @@ -using Avalonia.Media.Imaging; +using System.Globalization; +using System.IO; +using Avalonia.Media.Imaging; namespace SourceGit.Models { @@ -9,10 +11,17 @@ namespace SourceGit.Models public class RevisionImageFile { - public Bitmap Image { get; set; } = null; - public long FileSize { get; set; } = 0; - public string ImageType { get; set; } = string.Empty; + public Bitmap Image { get; } + public long FileSize { get; } + public string ImageType { get; } public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0"; + + public RevisionImageFile(string file, Bitmap img, long size) + { + Image = img; + FileSize = size; + ImageType = Path.GetExtension(file)!.Substring(1).ToUpper(CultureInfo.CurrentCulture); + } } public class RevisionTextFile diff --git a/src/Models/SelfUpdate.cs b/src/Models/SelfUpdate.cs index e02f80d8..05fa6124 100644 --- a/src/Models/SelfUpdate.cs +++ b/src/Models/SelfUpdate.cs @@ -33,9 +33,7 @@ namespace SourceGit.Models } } - public class AlreadyUpToDate - { - } + public class AlreadyUpToDate; public class SelfUpdateFailed { diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index 3ada2cf9..7dfb2237 100644 --- a/src/Models/ShellOrTerminal.cs +++ b/src/Models/ShellOrTerminal.cs @@ -42,7 +42,8 @@ namespace SourceGit.Models new ShellOrTerminal("mac-terminal", "Terminal", ""), new ShellOrTerminal("iterm2", "iTerm", ""), new ShellOrTerminal("warp", "Warp", ""), - new ShellOrTerminal("ghostty", "Ghostty", "") + new ShellOrTerminal("ghostty", "Ghostty", ""), + new ShellOrTerminal("kitty", "kitty", "") }; } else @@ -58,6 +59,7 @@ namespace SourceGit.Models new ShellOrTerminal("foot", "Foot", "foot"), new ShellOrTerminal("wezterm", "WezTerm", "wezterm"), new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"), + new ShellOrTerminal("kitty", "kitty", "kitty"), new ShellOrTerminal("custom", "Custom", ""), }; } diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index 257b6d33..2b77be50 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -11,6 +11,24 @@ namespace SourceGit.Models public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; - public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime); + public string Subject + { + get + { + var idx = Message.IndexOf('\n', StringComparison.Ordinal); + return idx > 0 ? Message.Substring(0, idx).Trim() : Message; + } + } + + public string TimeStr + { + get + { + return DateTime.UnixEpoch + .AddSeconds(Time) + .ToLocalTime() + .ToString(DateTimeFormat.Active.DateTime); + } + } } } diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index 87a6eabd..a86380c3 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -11,54 +11,47 @@ using SkiaSharp; namespace SourceGit.Models { - public enum StaticsticsMode + public enum StatisticsMode { All, ThisMonth, ThisWeek, } - public class StaticsticsAuthor(User user, int count) + public class StatisticsAuthor(User user, int count) { public User User { get; set; } = user; public int Count { get; set; } = count; } - public class StaticsticsSample(DateTime time, int count) - { - public DateTime Time { get; set; } = time; - public int Count { get; set; } = count; - } - public class StatisticsReport { - public static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; - public int Total { get; set; } = 0; - public List Authors { get; set; } = new List(); - public List Series { get; set; } = new List(); - public List XAxes { get; set; } = new List(); - public List YAxes { get; set; } = new List(); + public List Authors { get; set; } = new(); + public List Series { get; set; } = new(); + public List XAxes { get; set; } = new(); + public List YAxes { get; set; } = new(); + public StatisticsAuthor SelectedAuthor { get => _selectedAuthor; set => ChangeAuthor(value); } - public StatisticsReport(StaticsticsMode mode, DateTime start) + public StatisticsReport(StatisticsMode mode, DateTime start) { _mode = mode; - YAxes = [new Axis() + YAxes.Add(new Axis() { TextSize = 10, MinLimit = 0, SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 } - }]; + }); - if (mode == StaticsticsMode.ThisWeek) + if (mode == StatisticsMode.ThisWeek) { for (int i = 0; i < 7; i++) _mapSamples.Add(start.AddDays(i), 0); XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => WEEKDAYS[(int)v.DayOfWeek]) { TextSize = 10 }); } - else if (mode == StaticsticsMode.ThisMonth) + else if (mode == StatisticsMode.ThisMonth) { var now = DateTime.Now; var maxDays = DateTime.DaysInMonth(now.Year, now.Month); @@ -77,8 +70,8 @@ namespace SourceGit.Models { Total++; - var normalized = DateTime.MinValue; - if (_mode == StaticsticsMode.ThisWeek || _mode == StaticsticsMode.ThisMonth) + DateTime normalized; + if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth) normalized = time.Date; else normalized = new DateTime(time.Year, time.Month, 1).ToLocalTime(); @@ -92,10 +85,30 @@ namespace SourceGit.Models _mapUsers[author] = vu + 1; else _mapUsers.Add(author, 1); + + if (_mapUserSamples.TryGetValue(author, out var vus)) + { + if (vus.TryGetValue(normalized, out var n)) + vus[normalized] = n + 1; + else + vus.Add(normalized, 1); + } + else + { + _mapUserSamples.Add(author, new Dictionary + { + { normalized, 1 } + }); + } } public void Complete() { + foreach (var kv in _mapUsers) + Authors.Add(new StatisticsAuthor(kv.Key, kv.Value)); + + Authors.Sort((l, r) => r.Count - l.Count); + var samples = new List(); foreach (var kv in _mapSamples) samples.Add(new DateTimePoint(kv.Key, kv.Value)); @@ -110,47 +123,89 @@ namespace SourceGit.Models } ); - foreach (var kv in _mapUsers) - Authors.Add(new StaticsticsAuthor(kv.Key, kv.Value)); - - Authors.Sort((l, r) => r.Count - l.Count); - _mapUsers.Clear(); _mapSamples.Clear(); } public void ChangeColor(uint color) { - if (Series is [ColumnSeries series]) - series.Fill = new SolidColorPaint(new SKColor(color)); + _fillColor = color; + + var fill = new SKColor(color); + + if (Series.Count > 0 && Series[0] is ColumnSeries total) + total.Fill = new SolidColorPaint(_selectedAuthor == null ? fill : fill.WithAlpha(51)); + + if (Series.Count > 1 && Series[1] is ColumnSeries user) + user.Fill = new SolidColorPaint(fill); } - private StaticsticsMode _mode = StaticsticsMode.All; - private Dictionary _mapUsers = new Dictionary(); - private Dictionary _mapSamples = new Dictionary(); + public void ChangeAuthor(StatisticsAuthor author) + { + if (author == _selectedAuthor) + return; + + _selectedAuthor = author; + Series.RemoveRange(1, Series.Count - 1); + if (author == null || !_mapUserSamples.TryGetValue(author.User, out var userSamples)) + { + ChangeColor(_fillColor); + return; + } + + var samples = new List(); + foreach (var kv in userSamples) + samples.Add(new DateTimePoint(kv.Key, kv.Value)); + + Series.Add( + new ColumnSeries() + { + Values = samples, + Stroke = null, + Fill = null, + Padding = 1, + } + ); + + ChangeColor(_fillColor); + } + + private static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; + private StatisticsMode _mode; + private Dictionary _mapUsers = new(); + private Dictionary _mapSamples = new(); + private Dictionary> _mapUserSamples = new(); + private StatisticsAuthor _selectedAuthor = null; + private uint _fillColor = 255; } public class Statistics { - public StatisticsReport All { get; set; } - public StatisticsReport Month { get; set; } - public StatisticsReport Week { get; set; } + public StatisticsReport All { get; } + public StatisticsReport Month { get; } + public StatisticsReport Week { get; } public Statistics() { - _today = DateTime.Now.ToLocalTime().Date; - var weekOffset = (7 + (int)_today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7; - _thisWeekStart = _today.AddDays(-weekOffset); - _thisMonthStart = _today.AddDays(1 - _today.Day); + var today = DateTime.Now.ToLocalTime().Date; + var weekOffset = (7 + (int)today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7; + _thisWeekStart = today.AddDays(-weekOffset); + _thisMonthStart = today.AddDays(1 - today.Day); - All = new StatisticsReport(StaticsticsMode.All, DateTime.MinValue); - Month = new StatisticsReport(StaticsticsMode.ThisMonth, _thisMonthStart); - Week = new StatisticsReport(StaticsticsMode.ThisWeek, _thisWeekStart); + All = new StatisticsReport(StatisticsMode.All, DateTime.MinValue); + Month = new StatisticsReport(StatisticsMode.ThisMonth, _thisMonthStart); + Week = new StatisticsReport(StatisticsMode.ThisWeek, _thisWeekStart); } public void AddCommit(string author, double timestamp) { - var user = User.FindOrAdd(author); + var emailIdx = author.IndexOf('±', StringComparison.Ordinal); + var email = author.Substring(emailIdx + 1).ToLower(CultureInfo.CurrentCulture); + if (!_users.TryGetValue(email, out var user)) + { + user = User.FindOrAdd(author); + _users.Add(email, user); + } var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); if (time >= _thisWeekStart) @@ -164,13 +219,15 @@ namespace SourceGit.Models public void Complete() { + _users.Clear(); + All.Complete(); Month.Complete(); Week.Complete(); } - private readonly DateTime _today; private readonly DateTime _thisMonthStart; private readonly DateTime _thisWeekStart; + private readonly Dictionary _users = new(); } } diff --git a/src/Models/Submodule.cs b/src/Models/Submodule.cs index ce00ac02..ca73a8de 100644 --- a/src/Models/Submodule.cs +++ b/src/Models/Submodule.cs @@ -1,8 +1,20 @@ namespace SourceGit.Models { + public enum SubmoduleStatus + { + Normal = 0, + NotInited, + RevisionChanged, + Unmerged, + Modified, + } + public class Submodule { - public string Path { get; set; } = ""; - public bool IsDirty { get; set; } = false; + public string Path { get; set; } = string.Empty; + public string SHA { get; set; } = string.Empty; + public string URL { get; set; } = string.Empty; + public SubmoduleStatus Status { get; set; } = SubmoduleStatus.Normal; + public bool IsDirty => Status > SubmoduleStatus.NotInited; } } diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 51681d93..87944637 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -5,13 +5,13 @@ namespace SourceGit.Models public enum TagSortMode { CreatorDate = 0, - NameInAscending, - NameInDescending, + Name, } public class Tag : ObservableObject { public string Name { get; set; } = string.Empty; + public bool IsAnnotated { get; set; } = false; public string SHA { get; set; } = string.Empty; public ulong CreatorDate { get; set; } = 0; public string Message { get; set; } = string.Empty; diff --git a/src/Models/TemplateEngine.cs b/src/Models/TemplateEngine.cs index 8472750c..8f60bd74 100644 --- a/src/Models/TemplateEngine.cs +++ b/src/Models/TemplateEngine.cs @@ -102,7 +102,7 @@ namespace SourceGit.Models private int? Integer() { var start = _pos; - while (Peek() is char c && c >= '0' && c <= '9') + while (Peek() is >= '0' and <= '9') { _pos++; } @@ -118,7 +118,7 @@ namespace SourceGit.Models // text token start var tok = _pos; bool esc = false; - while (Next() is char c) + while (Next() is { } c) { if (esc) { @@ -129,7 +129,7 @@ namespace SourceGit.Models { case ESCAPE: // allow to escape only \ and $ - if (Peek() is char nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR)) + if (Peek() is { } nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR)) { esc = true; FlushText(tok, _pos - 1); @@ -173,7 +173,7 @@ namespace SourceGit.Models if (Next() != VARIABLE_START) return null; int name_start = _pos; - while (Next() is char c) + while (Next() is { } c) { // name character, continue advancing if (IsNameChar(c)) @@ -228,7 +228,7 @@ namespace SourceGit.Models var sb = new StringBuilder(); var tok = _pos; var esc = false; - while (Next() is char c) + while (Next() is { } c) { if (esc) { @@ -277,7 +277,7 @@ namespace SourceGit.Models var sb = new StringBuilder(); var tok = _pos; var esc = false; - while (Next() is char c) + while (Next() is { } c) { if (esc) { @@ -402,7 +402,7 @@ namespace SourceGit.Models sb.AppendJoin(", ", paths); if (max < context.changes.Count) - sb.AppendFormat(" and {0} other files", context.changes.Count - max); + sb.Append($" and {context.changes.Count - max} other files"); return sb.ToString(); } diff --git a/src/Models/TextInlineChange.cs b/src/Models/TextInlineChange.cs index c96d839f..afe5bec4 100644 --- a/src/Models/TextInlineChange.cs +++ b/src/Models/TextInlineChange.cs @@ -2,30 +2,22 @@ namespace SourceGit.Models { - public class TextInlineChange + public class TextInlineChange(int dp, int dc, int ap, int ac) { - public int DeletedStart { get; set; } - public int DeletedCount { get; set; } - public int AddedStart { get; set; } - public int AddedCount { get; set; } + public int DeletedStart { get; set; } = dp; + public int DeletedCount { get; set; } = dc; + public int AddedStart { get; set; } = ap; + public int AddedCount { get; set; } = ac; - class Chunk + private class Chunk(int hash, int start, int size) { - public int Hash; + public readonly int Hash = hash; + public readonly int Start = start; + public readonly int Size = size; public bool Modified; - public int Start; - public int Size; - - public Chunk(int hash, int start, int size) - { - Hash = hash; - Modified = false; - Start = start; - Size = size; - } } - enum Edit + private enum Edit { None, DeletedRight, @@ -34,7 +26,7 @@ namespace SourceGit.Models AddedLeft, } - class EditResult + private class EditResult { public Edit State; public int DeleteStart; @@ -43,14 +35,6 @@ namespace SourceGit.Models public int AddEnd; } - public TextInlineChange(int dp, int dc, int ap, int ac) - { - DeletedStart = dp; - DeletedCount = dc; - AddedStart = ap; - AddedCount = ac; - } - public static List Compare(string oldValue, string newValue) { var hashes = new Dictionary(); @@ -204,11 +188,10 @@ namespace SourceGit.Models for (int i = 0; i <= half; i++) { - for (int j = -i; j <= i; j += 2) { var idx = j + half; - int o, n; + int o; if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1])) { o = forward[idx + 1]; @@ -220,7 +203,7 @@ namespace SourceGit.Models rs.State = Edit.DeletedRight; } - n = o - j; + var n = o - j; var startX = o; var startY = n; @@ -258,7 +241,7 @@ namespace SourceGit.Models for (int j = -i; j <= i; j += 2) { var idx = j + half; - int o, n; + int o; if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1])) { o = reverse[idx + 1] - 1; @@ -270,7 +253,7 @@ namespace SourceGit.Models rs.State = Edit.AddedLeft; } - n = o - (j + delta); + var n = o - (j + delta); var endX = o; var endY = n; @@ -312,8 +295,7 @@ namespace SourceGit.Models private static void AddChunk(List chunks, Dictionary hashes, string data, int start) { - int hash; - if (hashes.TryGetValue(data, out hash)) + if (hashes.TryGetValue(data, out var hash)) { chunks.Add(new Chunk(hash, start, data.Length)); } diff --git a/src/Models/User.cs b/src/Models/User.cs index 066ab747..0b4816fe 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -26,11 +26,7 @@ namespace SourceGit.Models public override bool Equals(object obj) { - if (obj == null || !(obj is User)) - return false; - - var other = obj as User; - return Name == other.Name && Email == other.Email; + return obj is User other && Name == other.Name && Email == other.Email; } public override int GetHashCode() diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 3ccecad4..4d6656e3 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -12,27 +12,50 @@ namespace SourceGit.Models { _repo = repo; - _wcWatcher = new FileSystemWatcher(); - _wcWatcher.Path = fullpath; - _wcWatcher.Filter = "*"; - _wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; - _wcWatcher.IncludeSubdirectories = true; - _wcWatcher.Created += OnWorkingCopyChanged; - _wcWatcher.Renamed += OnWorkingCopyChanged; - _wcWatcher.Changed += OnWorkingCopyChanged; - _wcWatcher.Deleted += OnWorkingCopyChanged; - _wcWatcher.EnableRaisingEvents = true; + var testGitDir = new DirectoryInfo(Path.Combine(fullpath, ".git")).FullName; + var desiredDir = new DirectoryInfo(gitDir).FullName; + if (testGitDir.Equals(desiredDir, StringComparison.Ordinal)) + { + var combined = new FileSystemWatcher(); + combined.Path = fullpath; + combined.Filter = "*"; + combined.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; + combined.IncludeSubdirectories = true; + combined.Created += OnRepositoryChanged; + combined.Renamed += OnRepositoryChanged; + combined.Changed += OnRepositoryChanged; + combined.Deleted += OnRepositoryChanged; + combined.EnableRaisingEvents = true; - _repoWatcher = new FileSystemWatcher(); - _repoWatcher.Path = gitDir; - _repoWatcher.Filter = "*"; - _repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; - _repoWatcher.IncludeSubdirectories = true; - _repoWatcher.Created += OnRepositoryChanged; - _repoWatcher.Renamed += OnRepositoryChanged; - _repoWatcher.Changed += OnRepositoryChanged; - _repoWatcher.Deleted += OnRepositoryChanged; - _repoWatcher.EnableRaisingEvents = true; + _watchers.Add(combined); + } + else + { + var wc = new FileSystemWatcher(); + wc.Path = fullpath; + wc.Filter = "*"; + wc.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; + wc.IncludeSubdirectories = true; + wc.Created += OnWorkingCopyChanged; + wc.Renamed += OnWorkingCopyChanged; + wc.Changed += OnWorkingCopyChanged; + wc.Deleted += OnWorkingCopyChanged; + wc.EnableRaisingEvents = true; + + var git = new FileSystemWatcher(); + git.Path = gitDir; + git.Filter = "*"; + git.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; + git.IncludeSubdirectories = true; + git.Created += OnGitDirChanged; + git.Renamed += OnGitDirChanged; + git.Changed += OnGitDirChanged; + git.Deleted += OnGitDirChanged; + git.EnableRaisingEvents = true; + + _watchers.Add(wc); + _watchers.Add(git); + } _timer = new Timer(Tick, null, 100, 100); } @@ -77,22 +100,13 @@ namespace SourceGit.Models public void Dispose() { - _repoWatcher.EnableRaisingEvents = false; - _repoWatcher.Created -= OnRepositoryChanged; - _repoWatcher.Renamed -= OnRepositoryChanged; - _repoWatcher.Changed -= OnRepositoryChanged; - _repoWatcher.Deleted -= OnRepositoryChanged; - _repoWatcher.Dispose(); - _repoWatcher = null; - - _wcWatcher.EnableRaisingEvents = false; - _wcWatcher.Created -= OnWorkingCopyChanged; - _wcWatcher.Renamed -= OnWorkingCopyChanged; - _wcWatcher.Changed -= OnWorkingCopyChanged; - _wcWatcher.Deleted -= OnWorkingCopyChanged; - _wcWatcher.Dispose(); - _wcWatcher = null; + foreach (var watcher in _watchers) + { + watcher.EnableRaisingEvents = false; + watcher.Dispose(); + } + _watchers.Clear(); _timer.Dispose(); _timer = null; } @@ -107,7 +121,6 @@ namespace SourceGit.Models { _updateBranch = 0; _updateWC = 0; - _updateSubmodules = 0; if (_updateTags > 0) { @@ -115,10 +128,15 @@ namespace SourceGit.Models Task.Run(_repo.RefreshTags); } + if (_updateSubmodules > 0 || _repo.MayHaveSubmodules()) + { + _updateSubmodules = 0; + Task.Run(_repo.RefreshSubmodules); + } + Task.Run(_repo.RefreshBranches); Task.Run(_repo.RefreshCommits); Task.Run(_repo.RefreshWorkingCopyChanges); - Task.Run(_repo.RefreshSubmodules); Task.Run(_repo.RefreshWorktrees); } @@ -150,14 +168,63 @@ namespace SourceGit.Models private void OnRepositoryChanged(object o, FileSystemEventArgs e) { - if (string.IsNullOrEmpty(e.Name) || e.Name.EndsWith(".lock", StringComparison.Ordinal)) + if (string.IsNullOrEmpty(e.Name) || e.Name.Equals(".git", StringComparison.Ordinal)) return; - var name = e.Name.Replace("\\", "/"); - if (name.StartsWith("modules", StringComparison.Ordinal) && name.EndsWith("HEAD", StringComparison.Ordinal)) + var name = e.Name.Replace('\\', '/').TrimEnd('/'); + if (name.EndsWith("/.git", StringComparison.Ordinal)) + return; + + if (name.StartsWith(".git/", StringComparison.Ordinal)) + HandleGitDirFileChanged(name.Substring(5)); + else + HandleWorkingCopyFileChanged(name); + } + + private void OnGitDirChanged(object o, FileSystemEventArgs e) + { + if (string.IsNullOrEmpty(e.Name)) + return; + + var name = e.Name.Replace('\\', '/').TrimEnd('/'); + HandleGitDirFileChanged(name); + } + + private void OnWorkingCopyChanged(object o, FileSystemEventArgs e) + { + if (string.IsNullOrEmpty(e.Name)) + return; + + var name = e.Name.Replace('\\', '/').TrimEnd('/'); + if (name.Equals(".git", StringComparison.Ordinal) || + name.StartsWith(".git/", StringComparison.Ordinal) || + name.EndsWith("/.git", StringComparison.Ordinal)) + return; + + HandleWorkingCopyFileChanged(name); + } + + private void HandleGitDirFileChanged(string name) + { + if (name.Contains("fsmonitor--daemon/", StringComparison.Ordinal) || + name.EndsWith(".lock", StringComparison.Ordinal) || + name.StartsWith("lfs/", StringComparison.Ordinal)) + return; + + if (name.StartsWith("modules", StringComparison.Ordinal)) { - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); - _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + if (name.EndsWith("/HEAD", StringComparison.Ordinal) || + name.EndsWith("/ORIG_HEAD", StringComparison.Ordinal)) + { + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + } + } + else if (name.Equals("MERGE_HEAD", StringComparison.Ordinal) || + name.Equals("AUTO_MERGE", StringComparison.Ordinal)) + { + if (_repo.MayHaveSubmodules()) + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); } else if (name.StartsWith("refs/tags", StringComparison.Ordinal)) { @@ -168,6 +235,7 @@ namespace SourceGit.Models _updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); } else if (name.Equals("HEAD", StringComparison.Ordinal) || + name.Equals("BISECT_START", StringComparison.Ordinal) || name.StartsWith("refs/heads/", StringComparison.Ordinal) || name.StartsWith("refs/remotes/", StringComparison.Ordinal) || (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) @@ -180,14 +248,16 @@ namespace SourceGit.Models } } - private void OnWorkingCopyChanged(object o, FileSystemEventArgs e) + private void HandleWorkingCopyFileChanged(string name) { - if (string.IsNullOrEmpty(e.Name)) + if (name.StartsWith(".vs/", StringComparison.Ordinal)) return; - var name = e.Name.Replace("\\", "/"); - if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal)) + if (name.Equals(".gitmodules", StringComparison.Ordinal)) + { + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); return; + } lock (_lockSubmodule) { @@ -205,8 +275,7 @@ namespace SourceGit.Models } private readonly IRepository _repo = null; - private FileSystemWatcher _repoWatcher = null; - private FileSystemWatcher _wcWatcher = null; + private List _watchers = []; private Timer _timer = null; private int _lockCount = 0; private long _updateWC = 0; @@ -215,7 +284,7 @@ namespace SourceGit.Models private long _updateStashes = 0; private long _updateTags = 0; - private object _lockSubmodule = new object(); + private readonly Lock _lockSubmodule = new(); private List _submodules = new List(); } } diff --git a/src/Models/Worktree.cs b/src/Models/Worktree.cs index bc40e320..26f88a8a 100644 --- a/src/Models/Worktree.cs +++ b/src/Models/Worktree.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { @@ -22,12 +23,12 @@ namespace SourceGit.Models get { if (IsDetached) - return $"deteched HEAD at {Head.Substring(10)}"; + return $"detached HEAD at {Head.AsSpan(10)}"; - if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal)) + if (Branch.StartsWith("refs/heads/", StringComparison.Ordinal)) return Branch.Substring(11); - if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal)) + if (Branch.StartsWith("refs/remotes/", StringComparison.Ordinal)) return Branch.Substring(13); return Branch; diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index bfb98500..3f6de903 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -5,6 +5,8 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; namespace SourceGit.Native { @@ -16,6 +18,21 @@ namespace SourceGit.Native builder.With(new X11PlatformOptions() { EnableIme = true }); } + public void SetupWindow(Window window) + { + if (OS.UseSystemWindowFrame) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; + window.ExtendClientAreaToDecorationsHint = false; + } + else + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + window.ExtendClientAreaToDecorationsHint = true; + window.Classes.Add("custom_window_frame"); + } + } + public string FindGitExecutable() { return FindExecutable("git"); @@ -103,8 +120,8 @@ namespace SourceGit.Native private string FindExecutable(string filename) { var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); - foreach (var path in pathes) + var paths = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in paths) { var test = Path.Combine(path, filename); if (File.Exists(test)) diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index d7ef4701..b76d239a 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -5,6 +5,8 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; namespace SourceGit.Native { @@ -36,6 +38,12 @@ namespace SourceGit.Native Environment.SetEnvironmentVariable("PATH", path); } + public void SetupWindow(Window window) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome; + window.ExtendClientAreaToDecorationsHint = true; + } + public string FindGitExecutable() { var gitPathVariants = new List() { @@ -59,6 +67,8 @@ namespace SourceGit.Native return "Warp"; case "ghostty": return "Ghostty"; + case "kitty": + return "kitty"; } return string.Empty; diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 320b5208..ad6f8104 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using Avalonia; +using Avalonia.Controls; namespace SourceGit.Native { @@ -14,6 +15,7 @@ namespace SourceGit.Native public interface IBackend { void SetupApp(AppBuilder builder); + void SetupWindow(Window window); string FindGitExecutable(); string FindTerminal(Models.ShellOrTerminal shell); @@ -68,6 +70,12 @@ namespace SourceGit.Native set; } = []; + public static bool UseSystemWindowFrame + { + get => OperatingSystem.IsLinux() && _enableSystemWindowFrame; + set => _enableSystemWindowFrame = value; + } + static OS() { if (OperatingSystem.IsWindows()) @@ -116,11 +124,16 @@ namespace SourceGit.Native Directory.CreateDirectory(DataDir); } - public static void SetupEnternalTools() + public static void SetupExternalTools() { ExternalTools = _backend.FindExternalTools(); } + public static void SetupForWindow(Window window) + { + _backend.SetupWindow(window); + } + public static string FindGitExecutable() { return _backend.FindGitExecutable(); @@ -225,5 +238,6 @@ namespace SourceGit.Native private static IBackend _backend = null; private static string _gitExecutable = string.Empty; + private static bool _enableSystemWindowFrame = false; } } diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index eb354f10..06d80bf0 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -8,6 +8,7 @@ using System.Text; using Avalonia; using Avalonia.Controls; +using Avalonia.Platform; using Avalonia.Threading; namespace SourceGit.Native @@ -15,16 +16,12 @@ namespace SourceGit.Native [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { - [StructLayout(LayoutKind.Sequential)] - internal struct RTL_OSVERSIONINFOEX + internal struct RECT { - internal uint dwOSVersionInfoSize; - internal uint dwMajorVersion; - internal uint dwMinorVersion; - internal uint dwBuildNumber; - internal uint dwPlatformId; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - internal string szCSDVersion; + public int left; + public int top; + public int right; + public int bottom; } [StructLayout(LayoutKind.Sequential)] @@ -36,9 +33,6 @@ namespace SourceGit.Native public int cyBottomHeight; } - [DllImport("ntdll.dll")] - private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation); - [DllImport("dwmapi.dll")] private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); @@ -54,41 +48,96 @@ namespace SourceGit.Native [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags); + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + public void SetupApp(AppBuilder builder) { // Fix drop shadow issue on Windows 10 - RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); - v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); - if (RtlGetVersion(ref v) == 0 && (v.dwMajorVersion < 10 || v.dwBuildNumber < 22000)) + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 22000)) { Window.WindowStateProperty.Changed.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); Control.LoadedEvent.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); } } + public void SetupWindow(Window window) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + window.ExtendClientAreaToDecorationsHint = true; + window.Classes.Add("fix_maximized_padding"); + + Win32Properties.AddWndProcHookCallback(window, (IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled) => + { + // Custom WM_NCHITTEST + if (msg == 0x0084) + { + handled = true; + + if (window.WindowState == WindowState.FullScreen || window.WindowState == WindowState.Maximized) + return 1; // HTCLIENT + + var p = IntPtrToPixelPoint(lParam); + GetWindowRect(hWnd, out var rcWindow); + + var borderThickness = (int)(4 * window.RenderScaling); + int y = 1; + int x = 1; + if (p.X >= rcWindow.left && p.X < rcWindow.left + borderThickness) + x = 0; + else if (p.X < rcWindow.right && p.X >= rcWindow.right - borderThickness) + x = 2; + + if (p.Y >= rcWindow.top && p.Y < rcWindow.top + borderThickness) + y = 0; + else if (p.Y < rcWindow.bottom && p.Y >= rcWindow.bottom - borderThickness) + y = 2; + + var zone = y * 3 + x; + switch (zone) + { + case 0: + return 13; // HTTOPLEFT + case 1: + return 12; // HTTOP + case 2: + return 14; // HTTOPRIGHT + case 3: + return 10; // HTLEFT + case 4: + return 1; // HTCLIENT + case 5: + return 11; // HTRIGHT + case 6: + return 16; // HTBOTTOMLEFT + case 7: + return 15; // HTBOTTOM + default: + return 17; // HTBOTTOMRIGHT + } + } + + return IntPtr.Zero; + }); + } + public string FindGitExecutable() { var reg = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64); - var git = reg.OpenSubKey("SOFTWARE\\GitForWindows"); - if (git != null && git.GetValue("InstallPath") is string installPath) - { + var git = reg.OpenSubKey(@"SOFTWARE\GitForWindows"); + if (git?.GetValue("InstallPath") is string installPath) return Path.Combine(installPath, "bin", "git.exe"); - } var builder = new StringBuilder("git.exe", 259); if (!PathFindOnPath(builder, null)) - { return null; - } var exePath = builder.ToString(); if (!string.IsNullOrEmpty(exePath)) - { return exePath; - } return null; } @@ -126,7 +175,7 @@ namespace SourceGit.Native break; case "cmd": - return "C:\\Windows\\System32\\cmd.exe"; + return @"C:\Windows\System32\cmd.exe"; case "wt": var wtFinder = new StringBuilder("wt.exe", 512); if (PathFindOnPath(wtFinder, null)) @@ -144,8 +193,8 @@ namespace SourceGit.Native finder.VSCode(FindVSCode); finder.VSCodeInsiders(FindVSCodeInsiders); finder.VSCodium(FindVSCodium); - finder.Fleet(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\Programs\\Fleet\\Fleet.exe"); - finder.FindJetBrainsFromToolbox(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\JetBrains\\Toolbox"); + finder.Fleet(() => $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Programs\Fleet\Fleet.exe"); + finder.FindJetBrainsFromToolbox(() => $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\JetBrains\Toolbox"); finder.SublimeText(FindSublimeText); finder.TryAdd("Visual Studio", "vs", FindVisualStudio, GenerateCommandlineArgsForVisualStudio); return finder.Founded; @@ -228,6 +277,12 @@ namespace SourceGit.Native }, DispatcherPriority.Render); } + private PixelPoint IntPtrToPixelPoint(IntPtr param) + { + var v = IntPtr.Size == 4 ? param.ToInt32() : (int)(param.ToInt64() & 0xFFFFFFFF); + return new PixelPoint((short)(v & 0xffff), (short)(v >> 16)); + } + #region EXTERNAL_EDITOR_FINDER private string FindVSCode() { @@ -238,9 +293,7 @@ namespace SourceGit.Native // VSCode (system) var systemVScode = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"); if (systemVScode != null) - { return systemVScode.GetValue("DisplayIcon") as string; - } var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.CurrentUser, @@ -249,9 +302,7 @@ namespace SourceGit.Native // VSCode (user) var vscode = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1"); if (vscode != null) - { return vscode.GetValue("DisplayIcon") as string; - } return string.Empty; } @@ -265,9 +316,7 @@ namespace SourceGit.Native // VSCode - Insiders (system) var systemVScodeInsiders = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"); if (systemVScodeInsiders != null) - { return systemVScodeInsiders.GetValue("DisplayIcon") as string; - } var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.CurrentUser, @@ -276,9 +325,7 @@ namespace SourceGit.Native // VSCode - Insiders (user) var vscodeInsiders = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1"); if (vscodeInsiders != null) - { return vscodeInsiders.GetValue("DisplayIcon") as string; - } return string.Empty; } @@ -290,11 +337,9 @@ namespace SourceGit.Native Microsoft.Win32.RegistryView.Registry64); // VSCodium (system) - var systemVScodium = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1"); - if (systemVScodium != null) - { - return systemVScodium.GetValue("DisplayIcon") as string; - } + var systemVSCodium = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1"); + if (systemVSCodium != null) + return systemVSCodium.GetValue("DisplayIcon") as string; var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.CurrentUser, @@ -303,9 +348,7 @@ namespace SourceGit.Native // VSCodium (user) var vscodium = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1"); if (vscodium != null) - { return vscodium.GetValue("DisplayIcon") as string; - } return string.Empty; } @@ -342,15 +385,13 @@ namespace SourceGit.Native Microsoft.Win32.RegistryView.Registry64); // Get default class for VisualStudio.Launcher.sln - the handler for *.sln files - if (localMachine.OpenSubKey(@"SOFTWARE\Classes\VisualStudio.Launcher.sln\CLSID") is Microsoft.Win32.RegistryKey launcher) + if (localMachine.OpenSubKey(@"SOFTWARE\Classes\VisualStudio.Launcher.sln\CLSID") is { } launcher) { // Get actual path to the executable if (launcher.GetValue(string.Empty) is string CLSID && - localMachine.OpenSubKey(@$"SOFTWARE\Classes\CLSID\{CLSID}\LocalServer32") is Microsoft.Win32.RegistryKey devenv && + localMachine.OpenSubKey(@$"SOFTWARE\Classes\CLSID\{CLSID}\LocalServer32") is { } devenv && devenv.GetValue(string.Empty) is string localServer32) - { return localServer32!.Trim('\"'); - } } return string.Empty; diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 66589440..001c7ee7 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -2,7 +2,9 @@ M41 512c0-128 46-241 138-333C271 87 384 41 512 41s241 46 333 138c92 92 138 205 138 333s-46 241-138 333c-92 92-205 138-333 138s-241-46-333-138C87 753 41 640 41 512zm87 0c0 108 36 195 113 271s164 113 271 113c108 0 195-36 271-113s113-164 113-271-36-195-113-271c-77-77-164-113-271-113-108 0-195 36-271 113C164 317 128 404 128 512zm256 148V292l195 113L768 512l-195 113-195 113v-77zm148-113-61 36V440l61 36 61 36-61 36z M304 464a128 128 0 01128-128c71 0 128 57 128 128v224a32 32 0 01-64 0V592h-128v95a32 32 0 01-64 0v-224zm64 1v64h128v-64a64 64 0 00-64-64c-35 0-64 29-64 64zM688 337c18 0 32 14 32 32v319a32 32 0 01-32 32c-18 0-32-14-32-32v-319a32 32 0 0132-32zM84 911l60-143A446 446 0 0164 512C64 265 265 64 512 64s448 201 448 448-201 448-448 448c-54 0-105-9-153-27l-242 22a32 32 0 01-32-44zm133-150-53 126 203-18 13 5c41 15 85 23 131 23 212 0 384-172 384-384S724 128 512 128 128 300 128 512c0 82 26 157 69 220l20 29z M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z + M851 755q0 23-16 39l-78 78q-16 16-39 16t-39-16l-168-168-168 168q-16 16-39 16t-39-16l-78-78q-16-16-16-39t16-39l168-168-168-168q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 168 168-168q16-16 39-16t39 16l78 78q16 16 16 39t-16 39l-168 168 168 168q16 16 16 39z M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z + M128 384a43 43 0 0043-43V213a43 43 0 0143-43h128a43 43 0 000-85H213a128 128 0 00-128 128v128a43 43 0 0043 43zm213 469H213a43 43 0 01-43-43v-128a43 43 0 00-85 0v128a128 128 0 00128 128h128a43 43 0 000-85zm384-299a43 43 0 000-85h-49A171 171 0 00555 347V299a43 43 0 00-85 0v49A171 171 0 00347 469H299a43 43 0 000 85h49A171 171 0 00469 677V725a43 43 0 0085 0v-49A171 171 0 00677 555zm-213 43a85 85 0 1185-85 85 85 0 01-85 85zm384 43a43 43 0 00-43 43v128a43 43 0 01-43 43h-128a43 43 0 000 85h128a128 128 0 00128-128v-128a43 43 0 00-43-43zM811 85h-128a43 43 0 000 85h128a43 43 0 0143 43v128a43 43 0 0085 0V213a128 128 0 00-128-128z M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z M832 64H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V96c0-18-14-32-32-32zM736 596 624 502 506 596V131h230v318z M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z @@ -46,6 +48,7 @@ M416 832H128V128h384v192C512 355 541 384 576 384L768 384v32c0 19 13 32 32 32S832 435 832 416v-64c0-6 0-19-6-25l-256-256c-6-6-19-6-25-6H128A64 64 0 0064 128v704C64 867 93 896 129 896h288c19 0 32-13 32-32S435 832 416 832zM576 172 722 320H576V172zM736 512C614 512 512 614 512 736S614 960 736 960s224-102 224-224S858 512 736 512zM576 736C576 646 646 576 736 576c32 0 58 6 83 26l-218 218c-19-26-26-51-26-83zm160 160c-32 0-64-13-96-32l224-224c19 26 32 58 32 96 0 90-70 160-160 160z M896 320c0-19-6-32-19-45l-192-192c-13-13-26-19-45-19H192c-38 0-64 26-64 64v768c0 38 26 64 64 64h640c38 0 64-26 64-64V320zm-256 384H384c-19 0-32-13-32-32s13-32 32-32h256c19 0 32 13 32 32s-13 32-32 32zm166-384H640V128l192 192h-26z M599 425 599 657 425 832 425 425 192 192 832 192Z + M505 74c-145 3-239 68-239 68-12 8-15 25-7 37 9 13 25 15 38 6 0 0 184-136 448 2 12 7 29 3 36-10 8-13 3-29-12-37-71-38-139-56-199-63-23-3-44-3-65-3m17 111c-254-3-376 201-376 201-8 12-5 29 7 37 12 8 29 4 39-10 0 0 103-178 329-175 226 3 325 173 325 173 8 12 24 17 37 9 14-8 17-24 9-37 0 0-117-195-370-199m-31 106c-72 5-140 31-192 74C197 449 132 603 204 811c5 14 20 21 34 17 14-5 21-20 16-34-66-191-7-316 79-388 84-69 233-85 343-17 54 34 96 93 118 151 22 58 20 114 3 141-18 28-54 38-86 30-32-8-58-31-59-80-1-73-58-118-118-125-57-7-123 24-140 92-32 125 49 302 238 361 14 4 29-3 34-17 4-14-3-29-18-34-163-51-225-206-202-297 10-41 46-55 84-52 37 4 69 26 69 73 2 70 48 117 100 131 52 13 112-3 144-52 33-50 28-120 3-188-26-68-73-136-140-178a356 356 0 00-213-52m15 104v0c-76 3-152 42-195 125-56 106-31 215 7 293 38 79 90 131 90 131 10 11 27 11 38 0s11-26 0-38c0 0-46-47-79-116s-54-157-8-244c48-90 133-111 208-90 76 22 140 88 138 186-2 15 9 28 24 29 15 1 27-10 29-27 3-122-79-210-176-239a246 246 0 00-75-9m9 213c-15 0-26 13-26 27 0 0 1 63 36 124 36 61 112 119 244 107 15-1 26-13 25-28-1-15-14-26-30-25-116 11-165-33-193-81-28-47-29-98-29-98a27 27 0 00-27-27z m211 611a142 142 0 00-90-4v-190a142 142 0 0090-4v198zm0 262v150h-90v-146a142 142 0 0090-4zm0-723a142 142 0 00-90-4v-146h90zm-51 246a115 115 0 11115-115 115 115 0 01-115 115zm0 461a115 115 0 11115-115 115 115 0 01-115 115zm256-691h563v90h-563zm0 461h563v90h-563zm0-282h422v90h-422zm0 474h422v90h-422z M853 267H514c-4 0-6-2-9-4l-38-66c-13-21-38-36-64-36H171c-41 0-75 34-75 75v555c0 41 34 75 75 75h683c41 0 75-34 75-75V341c0-41-34-75-75-75zm-683-43h233c4 0 6 2 9 4l38 66c13 21 38 36 64 36H853c6 0 11 4 11 11v75h-704V235c0-6 4-11 11-11zm683 576H171c-6 0-11-4-11-11V480h704V789c0 6-4 11-11 11z M1088 227H609L453 78a11 11 0 00-7-3H107a43 43 0 00-43 43v789a43 43 0 0043 43h981a43 43 0 0043-43V270a43 43 0 00-43-43zM757 599c0 5-5 9-10 9h-113v113c0 5-4 9-9 9h-56c-5 0-9-4-9-9V608h-113c-5 0-10-4-10-9V543c0-5 5-9 10-9h113V420c0-5 4-9 9-9h56c5 0 9 4 9 9V533h113c5 0 10 4 10 9v56z @@ -78,6 +81,7 @@ M512 0C233 0 7 223 0 500C6 258 190 64 416 64c230 0 416 200 416 448c0 53 43 96 96 96s96-43 96-96c0-283-229-512-512-512zm0 1023c279 0 505-223 512-500c-6 242-190 436-416 436c-230 0-416-200-416-448c0-53-43-96-96-96s-96 43-96 96c0 283 229 512 512 512z M976 0h-928A48 48 0 000 48v652a48 48 0 0048 48h416V928H200a48 48 0 000 96h624a48 48 0 000-96H560v-180h416a48 48 0 0048-48V48A48 48 0 00976 0zM928 652H96V96h832v556z M832 464h-68V240a128 128 0 00-128-128h-248a128 128 0 00-128 128v224H192c-18 0-32 14-32 32v384c0 18 14 32 32 32h640c18 0 32-14 32-32v-384c0-18-14-32-32-32zm-292 237v53a8 8 0 01-8 8h-40a8 8 0 01-8-8v-53a48 48 0 1156 0zm152-237H332V240a56 56 0 0156-56h248a56 56 0 0156 56v224z + M908 366h-25V248a18 18 0 00-0-2 20 20 0 00-5-13L681 7 681 7a19 19 0 00-4-3c-0-0-1-1-1-1a29 29 0 00-4-2L671 1a24 24 0 00-5-1H181a40 40 0 00-40 40v326h-25c-32 0-57 26-57 57v298c0 32 26 57 57 57h25v204c0 22 18 40 40 40H843a40 40 0 0040-40v-204h25c32 0 57-26 57-57V424a57 57 0 00-57-57zM181 40h465v205c0 11 9 20 20 20h177v101H181V40zm413 527c0 89-54 143-134 143-81 0-128-61-128-138 0-82 52-143 132-143 84 0 129 63 129 138zm-440 139V433h62v220h108v52h-170zm690 267H181v-193H843l0 193zm18-280a305 305 0 01-91 15c-50 0-86-12-111-37-25-23-39-59-38-99 0-90 66-142 155-142 35 0 62 7 76 13l-13 49c-15-6-33-12-63-12-51 0-90 29-90 88 0 56 35 89 86 89 14 0 25-2 30-4v-57h-42v-48h101v143zM397 570c0 53 25 91 66 91 42 0 65-40 65-92 0-49-23-91-66-91-42 0-66 40-66 93z M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z M824 645V307c0-56-46-102-102-102h-102V102l-154 154 154 154V307h102v338c-46 20-82 67-82 123 0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123zm-51 195c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72zM384 256c0-72-61-133-133-133-72 0-133 61-133 133 0 56 36 102 82 123v266C154 666 118 712 118 768c0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123V379C348 358 384 312 384 256zM323 768c0 41-31 72-72 72-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72zM251 328c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72z M896 64H128C96 64 64 96 64 128v768c0 32 32 64 64 64h768c32 0 64-32 64-64V128c0-32-32-64-64-64z m-64 736c0 16-17 32-32 32H224c-18 0-32-12-32-32V224c0-16 16-32 32-32h576c15 0 32 16 32 32v576zM512 384c-71 0-128 57-128 128s57 128 128 128 128-57 128-128-57-128-128-128z @@ -85,7 +89,8 @@ M299 811 299 725 384 725 384 811 299 811M469 811 469 725 555 725 555 811 469 811M640 811 640 725 725 725 725 811 640 811M299 640 299 555 384 555 384 640 299 640M469 640 469 555 555 555 555 640 469 640M640 640 640 555 725 555 725 640 640 640M299 469 299 384 384 384 384 469 299 469M469 469 469 384 555 384 555 469 469 469M640 469 640 384 725 384 725 469 640 469M299 299 299 213 384 213 384 299 299 299M469 299 469 213 555 213 555 299 469 299M640 299 640 213 725 213 725 299 640 299Z M64 363l0 204 265 0L329 460c0-11 6-18 14-20C349 437 355 437 362 441c93 60 226 149 226 149 33 22 34 60 0 82 0 0-133 89-226 149-14 9-32-3-32-18l-1-110L64 693l0 117c0 41 34 75 75 75l746 0c41 0 75-34 75-74L960 364c0-0 0-1 0-1L64 363zM64 214l0 75 650 0-33-80c-16-38-62-69-103-69l-440 0C97 139 64 173 64 214z M683 409v204L1024 308 683 0v191c-413 0-427 526-427 526c117-229 203-307 427-307zm85 492H102V327h153s38-63 114-122H51c-28 0-51 27-51 61v697c0 34 23 61 51 61h768c28 0 51-27 51-61V614l-102 100v187z - M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z + M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z + M512 0a512 512 0 01512 512 57 57 0 01-114 0 398 398 0 10-398 398 57 57 0 010 114A512 512 0 01512 0zm367 600 121 120a57 57 0 01-80 81l-40-40V967a57 57 0 01-50 57l-7 0a57 57 0 01-57-57v-205l-40 40a57 57 0 01-75 5l-5-5a57 57 0 01-0-80l120-121a80 80 0 01113-0zM512 272a57 57 0 0157 57V499h114a57 57 0 0156 50L740 556a57 57 0 01-57 57H512a57 57 0 01-57-57v-228a57 57 0 0150-57L512 272z M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z @@ -115,6 +120,7 @@ M558 545 790 403c24-15 31-47 16-71-15-24-46-31-70-17L507 457 277 315c-24-15-56-7-71 17-15 24-7 56 17 71l232 143V819c0 28 23 51 51 51 28 0 51-23 51-51V545h0zM507 0l443 256v512L507 1024 63 768v-512L507 0z M770 320a41 41 0 00-56-14l-252 153L207 306a41 41 0 10-43 70l255 153 2 296a41 41 0 0082 0l-2-295 255-155a41 41 0 0014-56zM481 935a42 42 0 01-42 0L105 741a42 42 0 01-21-36v-386a42 42 0 0121-36L439 89a42 42 0 0142 0l335 193a42 42 0 0121 36v87h84v-87a126 126 0 00-63-109L523 17a126 126 0 00-126 0L63 210a126 126 0 00-63 109v386a126 126 0 0063 109l335 193a126 126 0 00126 0l94-54-42-72zM1029 700h-126v-125a42 42 0 00-84 0v126h-126a42 42 0 000 84h126v126a42 42 0 1084 0v-126h126a42 42 0 000-84z M416 587c21 0 37 17 37 37v299A37 37 0 01416 960h-299a37 37 0 01-37-37v-299c0-21 17-37 37-37h299zm448 0c21 0 37 17 37 37v299A37 37 0 01864 960h-299a37 37 0 01-37-37v-299c0-21 17-37 37-37h299zM758 91l183 189a37 37 0 010 52l-182 188a37 37 0 01-53 1l-183-189a37 37 0 010-52l182-188a37 37 0 0153-1zM416 139c21 0 37 17 37 37v299A37 37 0 01416 512h-299a37 37 0 01-37-37v-299c0-21 17-37 37-37h299z + M653 435l-26 119H725c9 0 13 4 13 13v47c0 9-4 13-13 13h-107l-21 115c0 9-4 13-13 13h-47c-9 0-13-4-13-13l21-111H427l-21 115c0 9-4 13-13 13H346c-9 0-13-4-13-13l21-107h-85c-4-9-9-21-13-34v-38c0-9 4-13 13-13h98l26-119H294c-9 0-13-4-13-13V375c0-9 4-13 13-13h115l13-81c0-9 4-13 13-13h43c9 0 13 4 13 13L469 363h119l13-81c0-9 4-13 13-13h47c9 0 13 4 13 13l-13 77h85c9 0 13 4 13 13v47c0 9-4 13-13 13h-98v4zM512 0C230 0 0 230 0 512c0 145 60 282 166 375L90 1024H512c282 0 512-230 512-512S794 0 512 0zm-73 559h124l26-119h-128l-21 119z M875 128h-725A107 107 0 0043 235v555A107 107 0 00149 896h725a107 107 0 00107-107v-555A107 107 0 00875 128zm-115 640h-183v-58l25-3c15 0 19-8 14-24l-22-61H419l-28 82 39 2V768h-166v-58l18-3c18-2 22-11 26-24l125-363-40-4V256h168l160 448 39 3zM506 340l-72 218h145l-71-218h-2z M177 156c-22 5-33 17-36 37c-10 57-33 258-13 278l445 445c23 23 61 23 84 0l246-246c23-23 23-61 0-84l-445-445C437 120 231 145 177 156zM331 344c-26 26-69 26-95 0c-26-26-26-69 0-95s69-26 95 0C357 276 357 318 331 344z M683 537h-144v-142h-142V283H239a44 44 0 00-41 41v171a56 56 0 0014 34l321 321a41 41 0 0058 0l174-174a41 41 0 000-58zm-341-109a41 41 0 110-58a41 41 0 010 58zM649 284V142h-69v142h-142v68h142v142h69v-142h142v-68h-142z @@ -129,6 +135,7 @@ M762 1024C876 818 895 504 448 514V768L64 384l384-384v248c535-14 595 472 314 776z M832 464H332V240c0-31 25-56 56-56h248c31 0 56 25 56 56v68c0 4 4 8 8 8h56c4 0 8-4 8-8v-68c0-71-57-128-128-128H388c-71 0-128 57-128 128v224h-68c-18 0-32 14-32 32v384c0 18 14 32 32 32h640c18 0 32-14 32-32V496c0-18-14-32-32-32zM540 701v53c0 4-4 8-8 8h-40c-4 0-8-4-8-8v-53c-12-9-20-23-20-39 0-27 22-48 48-48s48 22 48 48c0 16-8 30-20 39z M170 831l343-342L855 831l105-105-448-448L64 726 170 831z + M667 607c-3-2-7-14-0-38 73-77 118-187 118-290C784 115 668 0 508 0 348 0 236 114 236 278c0 104 45 215 119 292 7 24-2 33-8 35C274 631 0 725 0 854L0 1024l1024 0 0-192C989 714 730 627 667 607L667 607z M880 128A722 722 0 01555 13a77 77 0 00-85 0 719 719 0 01-325 115c-40 4-71 38-71 80v369c0 246 329 446 439 446 110 0 439-200 439-446V207c0-41-31-76-71-80zM465 692a36 36 0 01-53 0L305 579a42 42 0 010-57 36 36 0 0153 0l80 85L678 353a36 36 0 0153 0 42 42 0 01-0 57L465 692z M812 864h-29V654c0-21-11-40-28-52l-133-88 134-89c18-12 28-31 28-52V164h28c18 0 32-14 32-32s-14-32-32-32H212c-18 0-32 14-32 32s14 32 32 32h30v210c0 21 11 40 28 52l133 88-134 89c-18 12-28 31-28 52V864H212c-18 0-32 14-32 32s14 32 32 32h600c18 0 32-14 32-32s-14-32-32-32zM441 566c18-12 28-31 28-52s-11-40-28-52L306 373V164h414v209l-136 90c-18 12-28 31-28 52 0 21 11 40 28 52l135 89V695c-9-7-20-13-32-19-30-15-93-41-176-41-63 0-125 14-175 38-12 6-22 12-31 18v-36l136-90z M0 512M1024 512M512 0M512 1024M762 412v100h-500v-100h-150v200h800v-200h-150z diff --git a/src/Resources/Images/ShellIcons/kitty.png b/src/Resources/Images/ShellIcons/kitty.png new file mode 100644 index 00000000..465c2863 Binary files /dev/null and b/src/Resources/Images/ShellIcons/kitty.png differ diff --git a/src/Resources/Images/github.png b/src/Resources/Images/github.png index 3a7abb16..d3c211da 100644 Binary files a/src/Resources/Images/github.png and b/src/Resources/Images/github.png differ diff --git a/src/Resources/Images/unreal.png b/src/Resources/Images/unreal.png index 4faae92b..01ceeb31 100644 Binary files a/src/Resources/Images/unreal.png and b/src/Resources/Images/unreal.png differ diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 25b54c60..99d9f77c 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -39,17 +39,25 @@ ALS UNVERÄNDERT ANGENOMMENE DATEIEN KEINE ALS UNVERÄNDERT ANGENOMMENEN DATEIEN ENTFERNEN + Bild laden... + Aktualisieren BINÄRE DATEI NICHT UNTERSTÜTZT!!! + Bisect + Abbrechen + Schlecht + Bisecting. Ist der aktuelle HEAD gut oder fehlerhaft? + Gut + Überspringen + Bisecting. Aktuellen Commit als gut oder schlecht markieren und einen anderen auschecken. Blame BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! Auschecken von ${0}$... - Mit HEAD vergleichen + Mit ${0}$ vergleichen Mit Worktree vergleichen Branch-Namen kopieren Benutzerdefinierte Aktion Lösche ${0}$... Lösche alle ausgewählten {0} Branches - Alle Änderungen verwerfen Fast-Forward zu ${0}$ Fetche ${0}$ in ${1}$ hinein... Git Flow - Abschließen ${0}$ @@ -60,8 +68,10 @@ Push ${0}$ Rebase ${0}$ auf ${1}$... Benenne ${0}$ um... + Setze ${0}$ zurück auf ${1}$... Setze verfolgten Branch... Branch Vergleich + Ungültiger upstream! Bytes ABBRECHEN Auf Vorgänger-Revision zurücksetzen @@ -78,7 +88,10 @@ Lokale Änderungen: Verwerfen Stashen & wieder anwenden + Alle Submodule updaten Branch: + Auschecken & Fast-Forward + Fast-Forward zu: Cherry Pick Quelle an Commit-Nachricht anhängen Commit(s): @@ -102,12 +115,16 @@ Mehrere cherry-picken Mit HEAD vergleichen Mit Worktree vergleichen - Info kopieren - SHA kopieren + Author + Committer + Information + SHA + Betreff Benutzerdefinierte Aktion Interactives Rebase von ${0}$ auf diesen Commit Merge in ${0}$ hinein Merge ... + Push ${0}$ zu ${1}$ Rebase von ${0}$ auf diesen Commit Reset ${0}$ auf diesen Commit Commit rückgängig machen @@ -116,6 +133,7 @@ Squash in den Vorgänger Squash Nachfolger Commits bis hier ÄNDERUNGEN + geänderte Datei(en) Änderungen durchsuchen... DATEIEN LFS DATEI @@ -135,6 +153,7 @@ SHA Im Browser öffnen Details + Betreff Commit-Nachricht Repository Einstellungen COMMIT TEMPLATE @@ -149,13 +168,16 @@ Branch Commit Repository + Auf Beenden der Aktion warten Email Adresse Email Adresse GIT Remotes automatisch fetchen Minute(n) Standard Remote + Bevorzugter Merge Modus TICKETSYSTEM + Beispiel Azure DevOps Rule hinzufügen Beispiel für Gitee Issue Regel einfügen Beispiel für Gitee Pull Request Regel einfügen Beispiel für Github-Regel hinzufügen @@ -176,7 +198,12 @@ Benutzername für dieses Repository Arbeitsplätze Farbe + Name Zuletzt geöffnete Tabs beim Starten wiederherstellen + WEITER + Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty)? + ALLES STAGEN & COMMITTEN + Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty) oder alle Änderungen stagen und dann committen? Konventionelle Commit-Hilfe Breaking Change: Geschlossenes Ticket: @@ -186,6 +213,7 @@ Typ der Änderung: Kopieren Kopiere gesamten Text + Ganzen Pfad kopieren Pfad kopieren Branch erstellen... Basierend auf: @@ -197,6 +225,7 @@ Branch-Namen eingeben. Leerzeichen werden durch Bindestriche ersetzt. Lokalen Branch erstellen + Überschreibe existierenden Branch Tag erstellen... Neuer Tag auf: Mit GPG signieren @@ -211,6 +240,9 @@ Ohne Anmerkung Halte Strg gedrückt, um direkt auszuführen Ausschneiden + De-initialisiere Submodul + Erzwinge De-Initialisierung, selbst wenn es lokale Änderungen enthält. + Submodul: Branch löschen Branch: Du löscht gerade einen Remote-Branch!!! @@ -235,7 +267,9 @@ ALT Kopieren Dateimodus geändert + Erste Differenz Ignoriere Leerzeichenänderungen + Letzte Differenz LFS OBJEKT ÄNDERUNG Nächste Änderung KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN @@ -244,6 +278,7 @@ Zeige versteckte Symbole Nebeneinander SUBMODUL + GELÖSCHT NEU Seiten wechseln Syntax Hervorhebung @@ -268,7 +303,6 @@ Ausgewähltes Repository bearbeiten Führe benutzerdefinierte Aktion aus Name der Aktion: - Fast-Forward (ohne Auschecken) Fetch Alle Remotes fetchen Aktiviere '--force' Option @@ -303,6 +337,8 @@ FLOW - Finish Hotfix FLOW - Finish Release Ziel: + Push zu Remote(s) nach Durchführung des Finish + Squash beim Merge Hotfix: Hotfix-Prefix: Git-Flow initialisieren @@ -364,6 +400,8 @@ Zum vorherigen Tab wechseln Neuen Tab erstellen Einstellungen öffnen + Aktiven Arbeitsplatz wechseln + Aktiven Tab wechseln REPOSITORY Gestagte Änderungen committen Gestagte Änderungen committen und pushen @@ -384,6 +422,7 @@ Suchpanel schließen Suche nächste Übereinstimmung Suche vorherige Übereinstimmung + Öffne mit externem Diff/Merge Tool Öffne Suchpanel Verwerfen Stagen @@ -405,7 +444,10 @@ In Browser öffnen FEHLER INFO + Arbeitsplätze + Tabs Branch mergen + Merge-Nachricht anpassen Ziel-Branch: Merge Option: Quelle: @@ -438,6 +480,7 @@ Vor {0} Monaten Vor {0} Jahren Gestern + Verwende 'Shift+Enter' um eine neue Zeile einzufügen. 'Enter' ist das Kürzel für den OK Button Einstellungen OPEN AI Analysierung des Diff Befehl @@ -446,8 +489,10 @@ Modell Name Server + Streaming aktivieren DARSTELLUNG Standardschriftart + Editor Tab Breite Schriftgröße Standard Texteditor @@ -468,6 +513,7 @@ Commit-Historie Zeige Autor Zeitpunkt anstatt Commit Zeitpunkt Zeige Nachfolger in den Commit Details + Zeige Tags im Commit Graph Längenvorgabe für Commit-Nachrichten GIT Aktiviere Auto-CRLF @@ -475,7 +521,8 @@ Benutzer Email Globale Git Benutzer Email Aktivere --prune beim fetchen - Diese App setzt Git (>= 2.23.0) voraus + Aktiviere --ignore-cr-at-eol beim Unterschied + Diese App setzt Git (>= 2.25.1) voraus Installationspfad Aktiviere HTTP SSL Verifizierung Benutzername @@ -499,12 +546,11 @@ Worktree Informationen in `$GIT_COMMON_DIR/worktrees` löschen Pull Remote-Branch: - Alle Branches fetchen Lokaler Branch: Lokale Änderungen: Verwerfen Stashen & wieder anwenden - Ohne Tags fetchen + Alle Submodule aktualisieren Remote: Pull (Fetch & Merge) Rebase anstatt Merge verwenden @@ -513,6 +559,8 @@ Erzwinge Push Lokaler Branch: Remote: + Revision: + Push Revision zu Remote-Branch Push Remote-Branch: Remote-Branch verfolgen @@ -526,7 +574,6 @@ Lokale Änderungen stashen & wieder anwenden Auf: Rebase: - Aktualisieren Remote hinzufügen Remote bearbeiten Name: @@ -548,13 +595,18 @@ Branch: ABBRECHEN Änderungen automatisch von Remote fetchen... + Sortieren + Nach Commit Datum + Nach Name Aufräumen (GC & Prune) Führt `git gc` auf diesem Repository aus. Filter aufheben + Löschen Repository Einstellungen WEITER Benutzerdefinierte Aktionen Keine benutzerdefinierten Aktionen + Alle Änderungen verwerfen Aktiviere '--reflog' Option Öffne im Datei-Browser Suche Branches/Tags/Submodule @@ -582,10 +634,12 @@ Commit suchen Autor Committer + Inhalt Dateiname Commit-Nachricht SHA Aktueller Branch + Submodule als Baum anzeigen Zeige Tags als Baum ÜBERSPRINGEN Statistiken @@ -595,11 +649,12 @@ TAGS NEUER TAG Nach Erstellungsdatum - Nach Namen (Aufsteigend) - Nach Namen (Absteigend) + Nach Namen Sortiere Öffne im Terminal Verwende relative Zeitangaben in Verlauf + Logs ansehen + Öffne '{0}' im Browser WORKTREES WORKTREE HINZUFÜGEN PRUNE @@ -608,12 +663,14 @@ Rücksetzmodus: Verschiebe zu: Aktueller Branch: + Reset Branch (ohne Checkout) + Auf: + Branch: Zeige im Datei-Explorer Commit rückgängig machen Commit: Commit Änderungen rückgängig machen Commit Nachricht umformulieren - Verwende 'Shift+Enter' um eine neue Zeile einzufügen. 'Enter' ist das Kürzel für den OK Button Bitte warten... SPEICHERN Speichern als... @@ -639,17 +696,17 @@ Pfad zum privaten SSH Schlüssel START Stash - Automatisch wiederherstellen nach dem Stashen - Die Arbeitsdateien bleiben unverändert, aber ein Stash wird gespeichert. Inklusive nicht-verfolgter Dateien - Behalte gestagte Dateien Name: - Optional. Name dieses Stashes + Optional. Informationen zu dieses Stashes + Modus: Nur gestagte Änderungen Gestagte und unstagte Änderungen der ausgewähleten Datei(en) werden gestasht!!! Lokale Änderungen stashen Anwenden + Kopiere Nachricht Entfernen + Als Path speichern... Stash entfernen Entfernen: Stashes @@ -666,11 +723,18 @@ SUBMODULE Submodul hinzufügen Relativen Pfad kopieren + De-initialisiere Submodul Untergeordnete Submodule fetchen Öffne Submodul Repository Relativer Pfad: Relativer Ordner um dieses Submodul zu speichern. Submodul löschen + STATUS + geändert + nicht initialisiert + Revision geändert + nicht gemerged + URL OK Tag-Namen kopieren Tag-Nachricht kopieren @@ -684,6 +748,10 @@ Submodul: Verwende `--remote` Option URL: + Logs + ALLES LÖSCHEN + Kopieren + Löschen Warnung Willkommensseite Erstelle Gruppe @@ -703,7 +771,7 @@ Git Ignore Ignoriere alle *{0} Dateien Ignoriere *{0} Datein im selben Ordner - Ignoriere Dateien im selben Ordner + Ignoriere nicht-verfolgte Dateien in diesem Ordner Ignoriere nur diese Datei Amend Du kannst diese Datei jetzt stagen. @@ -713,11 +781,17 @@ Klick-Ereignis auslösen Commit (Bearbeitung) Alle Änderungen stagen und committen + Du hast {0} Datei(en) gestaged, aber nur {1} werden angezeigt ({2} sind herausgefiltert). Willst du trotzdem fortfahren? KONFLIKTE ERKANNT + EXTERNES MERGE-TOOL ÖFFNEN + ALLE KONFLIKTE IN EXTERNEM MERGE-TOOL ÖFFNEN DATEI KONFLIKTE GELÖST + MEINE VERSION VERWENDEN + DEREN VERSION VERWENDEN NICHT-VERFOLGTE DATEIEN INKLUDIEREN KEINE BISHERIGEN COMMIT-NACHRICHTEN KEINE COMMIT TEMPLATES + Autor zurücksetzen Rechtsklick auf selektierte Dateien und wähle die Konfliktlösungen aus. SignOff GESTAGED @@ -726,7 +800,7 @@ UNSTAGED STAGEN ALLES STAGEN - ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN + ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN Template: ${0}$ ARBEITSPLATZ: Arbeitsplätze konfigurieren... diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index ad71427b..d4b4d26f 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -3,15 +3,15 @@ About SourceGit Opensource & Free Git GUI Client Add Worktree - What to Checkout: - Existing Branch - Create New Branch Location: Path for this worktree. Relative path is supported. Branch Name: Optional. Default is the destination folder name. Track Branch: Tracking remote branch + What to Checkout: + Create New Branch + Existing Branch AI Assistant RE-GENERATE Use AI to generate commit message @@ -35,17 +35,25 @@ FILES ASSUME UNCHANGED NO FILES ASSUMED AS UNCHANGED REMOVE + Load Image... + Refresh BINARY FILE NOT SUPPORTED!!! + Bisect + Abort + Bad + Bisecting. Is current HEAD good or bad? + Good + Skip + Bisecting. Mark current commit as good or bad and checkout another one. Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout ${0}$... - Compare with HEAD + Compare with ${0}$ Compare with Worktree Copy Branch Name Custom Action Delete ${0}$... Delete selected {0} branches - Discard all changes Fast-Forward to ${0}$ Fetch ${0}$ into ${1}$... Git Flow - Finish ${0}$ @@ -56,13 +64,14 @@ Push ${0}$ Rebase ${0}$ on ${1}$... Rename ${0}$... + Reset ${0}$ to ${1}$... Set Tracking Branch... Branch Compare Invalid upstream! Bytes CANCEL - Reset to This Revision Reset to Parent Revision + Reset to This Revision Generate commit message CHANGE DISPLAY MODE Show as File and Dir List @@ -70,12 +79,15 @@ Show as Filesystem Tree Checkout Branch Checkout Commit - Warning: By doing a commit checkout, your Head will be detached Commit: - Branch: + Warning: By doing a commit checkout, your Head will be detached Local Changes: Discard Stash & Reapply + Update all submodules + Branch: + Checkout & Fast-Forward + Fast-Forward to: Cherry Pick Append source to commit message Commit(s): @@ -94,17 +106,21 @@ Repository URL: CLOSE Editor + Checkout Commit Cherry-Pick Commit Cherry-Pick ... - Checkout Commit Compare with HEAD Compare with Worktree - Copy Info - Copy SHA + Author + Committer + Information + SHA + Subject Custom Action Interactively Rebase ${0}$ on Here Merge to ${0}$ Merge ... + Push ${0}$ to ${1}$ Rebase ${0}$ on Here Reset ${0}$ to Here Revert Commit @@ -113,6 +129,7 @@ Squash into Parent Squash Children into Here CHANGES + changed file(s) Search Changes... FILES LFS File @@ -131,12 +148,13 @@ REFS SHA Open in Browser - Enter commit subject Description + SUBJECT + Enter commit subject Repository Configure COMMIT TEMPLATE - Template Name: Template Content: + Template Name: CUSTOM ACTION Arguments: ${REPO} - Repository's path; ${BRANCH} - Selected branch; ${SHA} - Selected commit's SHA @@ -155,13 +173,13 @@ Default Remote Preferred Merge Mode ISSUE TRACKER + Add Sample Azure DevOps Rule Add Sample Gitee Issue Rule Add Sample Gitee Pull Request Rule Add Sample Github Rule Add Sample GitLab Issue Rule Add Sample GitLab Merge Request Rule Add Sample Jira Rule - Add Sample Azure DevOps Rule New Rule Issue Regex Expression: Rule Name: @@ -176,6 +194,7 @@ User name for this repository Workspaces Color + Name Restore tabs on startup CONTINUE Empty commit detected! Do you want to continue (--allow-empty)? @@ -190,8 +209,8 @@ Type of Change: Copy Copy All Text - Copy Path Copy Full Path + Copy Path Create Branch... Based On: Check out the created branch @@ -202,6 +221,7 @@ Enter branch name. Spaces will be replaced with dashes. Create Local Branch + Overwrite existing branch Create Tag... New Tag At: GPG signing @@ -216,6 +236,9 @@ lightweight Hold Ctrl to start directly Cut + De-initialize Submodule + Force de-init even if it contains local changes. + Submodule: Delete Branch Branch: You are about to delete a remote branch!!! @@ -227,8 +250,8 @@ Path: Target: All children will be removed from list. - Confirm Deleting Group This will only remove it from list, not from disk! + Confirm Deleting Group Confirm Deleting Repository Delete Submodule Submodule Path: @@ -241,7 +264,7 @@ Copy File Mode Changed First Difference - Ignore Whitespace Change + Ignore All Whitespace Changes Last Difference LFS OBJECT CHANGE Next Difference @@ -251,6 +274,7 @@ Show hidden symbols Side-By-Side Diff SUBMODULE + DELETED NEW Swap Syntax Highlighting @@ -275,7 +299,6 @@ Edit Selected Repository Run Custom Action Action Name: - Fast-Forward (without checkout) Fetch Fetch all remotes Force override local refs @@ -297,11 +320,11 @@ Unstage Unstage {0} files Unstage Changes in Selected Line(s) - Use Theirs (checkout --theirs) Use Mine (checkout --ours) + Use Theirs (checkout --theirs) File History - CONTENT CHANGE + CONTENT Git-Flow Development Branch: Feature: @@ -310,6 +333,8 @@ FLOW - Finish Hotfix FLOW - Finish Release Target: + Push to remote(s) after performing finish + Squash during merge Hotfix: Hotfix Prefix: Initialize Git-Flow @@ -331,8 +356,8 @@ Custom Pattern: Add Track Pattern to Git LFS Fetch - Fetch LFS Objects Run `git lfs fetch` to download Git LFS objects. This does not update the working copy. + Fetch LFS Objects Install Git LFS hooks Show Locks No Locked Files @@ -344,11 +369,11 @@ Prune Run `git lfs prune` to delete old LFS files from local storage Pull - Pull LFS Objects Run `git lfs pull` to download all Git LFS files for current ref & checkout + Pull LFS Objects Push - Push LFS Objects Push queued large files to the Git LFS endpoint + Push LFS Objects Remote: Track files named '{0}' Track all *{0} files @@ -367,10 +392,12 @@ Cancel current popup Clone new repository Close current page - Go to previous page Go to next page + Go to previous page Create new page Open Preferences dialog + Switch active workspace + Switch active page REPOSITORY Commit staged changes Commit and push staged changes @@ -379,11 +406,11 @@ Discard selected changes Fetch, starts directly Dashboard mode (Default) + Commit search mode Pull, starts directly Push, starts directly Force to reload this repository Stage/Unstage selected changes - Commit search mode Switch to 'Changes' Switch to 'Histories' Switch to 'Stashes' @@ -391,10 +418,11 @@ Close search panel Find next match Find previous match + Open with external diff/merge tool Open search panel + Discard Stage Unstage - Discard Initialize Repository Path: Cherry-Pick in progress. @@ -406,13 +434,16 @@ Revert in progress. Reverting commit Interactive Rebase - Target Branch: On: - Open in Browser + Target Branch: Copy Link + Open in Browser ERROR NOTICE + Workspaces + Pages Merge Branch + Customize merge message Into: Merge Option: Source: @@ -435,16 +466,17 @@ Copy Repository Path Repositories Paste - Just now - {0} minutes ago + {0} days ago 1 hour ago {0} hours ago - Yesterday - {0} days ago + Just now Last month - {0} months ago Last year + {0} minutes ago + {0} months ago {0} years ago + Yesterday + Use 'Shift+Enter' to input a new line. 'Enter' is the hotkey of OK button Preferences AI Analyze Diff Prompt @@ -485,36 +517,36 @@ User Email Global git user email Enable --prune on fetch + Enable --ignore-cr-at-eol in diff + Git (>= 2.25.1) is required by this app Install Path Enable HTTP SSL Verify User Name Global git user name Git version - Git (>= 2.23.0) is required by this app GPG SIGNING Commit GPG signing - Tag GPG signing GPG Format Program Install Path Input path for installed gpg program + Tag GPG signing User Signing Key User's gpg signing key INTEGRATION SHELL/TERMINAL - Shell/Terminal Path + Shell/Terminal Prune Remote Target: Prune Worktrees Prune worktree information in `$GIT_COMMON_DIR/worktrees` Pull Remote Branch: - Fetch all branches Into: Local Changes: Discard Stash & Reapply - Fetch without tags + Update all submodules Remote: Pull (Fetch & Merge) Use rebase instead of merge @@ -523,6 +555,8 @@ Force push Local Branch: Remote: + Revision: + Push Revision To Remote Push Changes To Remote Remote Branch: Set as tracking branch @@ -536,7 +570,6 @@ Stash & reapply local changes On: Rebase: - Refresh Add Remote Edit Remote Name: @@ -558,13 +591,18 @@ Branch: ABORT Auto fetching changes from remotes... + Sort + By Committer Date + By Name Cleanup(GC & Prune) Run `git gc` command for this repository. Clear all + Clear Configure this repository CONTINUE Custom Actions No Custom Actions + Discard all changes Enable '--reflog' Option Open in File Browser Search Branches/Tags/Submodules @@ -588,42 +626,47 @@ Open in External Tools Refresh REMOTES - ADD REMOTE + Add Remote Search Commit Author Committer + Content File Message SHA Current Branch + Show Submodules as Tree Show Tags as Tree SKIP Statistics SUBMODULES - ADD SUBMODULE - UPDATE SUBMODULE + Add Submodule + Update Submodule TAGS - NEW TAG + New Tag By Creator Date - By Name (Ascending) - By Name (Descending) + By Name Sort Open in Terminal Use relative time in histories + View Logs + Visit '{0}' in Browser WORKTREES - ADD WORKTREE - PRUNE + Add Worktree + Prune Git Repository URL Reset Current Branch To Revision Reset Mode: Move To: Current Branch: + Reset Branch (Without Checkout) + Move To: + Branch: Reveal in File Explorer Revert Commit Commit: Commit revert changes Reword Commit Message - Use 'Shift+Enter' to input a new line. 'Enter' is the hotkey of OK button Running. Please wait... SAVE Save As... @@ -649,16 +692,15 @@ Private SSH key store path START Stash - Auto-restore after stashing - Your working files remain unchanged, but a stash is saved. Include untracked files - Keep staged files Message: - Optional. Name of this stash + Optional. Message of this stash + Mode: Only staged changes Both staged and unstaged changes of selected file(s) will be stashed!!! Stash Local Changes Apply + Copy Message Drop Save as Patch... Drop Stash @@ -669,32 +711,43 @@ Statistics COMMITS COMMITTER + OVERVIEW MONTH WEEK - COMMITS: AUTHORS: - OVERVIEW + COMMITS: SUBMODULES Add Submodule Copy Relative Path + De-initialize Submodule Fetch nested submodules Open Submodule Repository Relative Path: Relative folder to store this module. Delete Submodule + STATUS + modified + not initialized + revision changed + unmerged + URL OK Copy Tag Name Copy Tag Message Delete ${0}$... Merge ${0}$ into ${1}$... Push ${0}$... - URL: Update Submodules All submodules Initialize as needed Recursively Submodule: Use --remote option + URL: + Logs + CLEAR ALL + Copy + Delete Warning Welcome Page Create Group @@ -714,7 +767,7 @@ Git Ignore Ignore all *{0} files Ignore *{0} files in the same folder - Ignore files in the same folder + Ignore untracked files in this folder Ignore this file only Amend You can stage this file now. @@ -734,6 +787,8 @@ INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES + Reset Author + Right-click the selected file(s), and make your choice to resolve conflicts. SignOff STAGED UNSTAGE @@ -741,9 +796,8 @@ UNSTAGED STAGE STAGE ALL - VIEW ASSUME UNCHANGED + VIEW ASSUME UNCHANGED Template: ${0}$ - Right-click the selected file(s), and make your choice to resolve conflicts. WORKSPACE: Configure Workspaces... WORKTREE diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index e2af0860..d66e515a 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -39,17 +39,25 @@ ARCHIVOS ASUMIDOS COMO SIN CAMBIOS NO HAY ARCHIVOS ASUMIDOS COMO SIN CAMBIOS REMOVER + Cargar Imagen... + Refrescar ¡ARCHIVO BINARIO NO SOPORTADO! + Bisect + Abortar + Malo + Bisecting. ¿Es el HEAD actual bueno o malo? + Bueno + Saltar + Bisecting. Marcar el commit actual cómo bueno o malo y revisar otro. Blame ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! Checkout ${0}$... - Comparar con HEAD + Comparar con ${0}$ Comparar con Worktree Copiar Nombre de Rama Acción personalizada Eliminar ${0}$... Eliminar {0} ramas seleccionadas - Descartar todos los cambios Fast-Forward a ${0}$ Fetch ${0}$ en ${1}$... Git Flow - Finalizar ${0}$ @@ -60,6 +68,7 @@ Push ${0}$ Rebase ${0}$ en ${1}$... Renombrar ${0}$... + Resetear ${0}$ a ${1}$... Establecer Rama de Seguimiento... Comparar Ramas ¡Upstream inválido! @@ -79,7 +88,10 @@ Cambios Locales: Descartar Stash & Reaplicar + Actualizar todos los submódulos Rama: + Checkout & Fast-Forward + Fast-Forward a: Cherry Pick Añadir fuente al mensaje de commit Commit(s): @@ -94,7 +106,7 @@ Nombre Local: Nombre del repositorio. Opcional. Carpeta Padre: - Inicializar y actualizar submodulos + Inicializar y actualizar submódulos URL del Repositorio: CERRAR Editor @@ -103,8 +115,11 @@ Cherry-Pick ... Comparar con HEAD Comparar con Worktree - Copiar Información - Copiar SHA + Autor + Committer + Información + SHA + Asunto Acción personalizada Rebase Interactivo ${0}$ hasta Aquí Merge a ${0}$ @@ -117,6 +132,7 @@ Squash en Parent Squash Commits Hijos hasta Aquí CAMBIOS + archivo(s) modificado(s) Buscar Cambios... ARCHIVOS Archivo LFS @@ -136,6 +152,7 @@ SHA Abrir en Navegador Descripción + ASUNTO Introducir asunto del commit Configurar Repositorio PLANTILLA DE COMMIT @@ -157,6 +174,7 @@ Fetch remotos automáticamente Minuto(s) Remoto por Defecto + Modo preferido de Merge SEGUIMIENTO DE INCIDENCIAS Añadir Regla de Ejemplo para Azure DevOps Añadir Regla de Ejemplo para Incidencias de Gitee @@ -179,7 +197,12 @@ Nombre de usuario para este repositorio Espacios de Trabajo Color + Nombre Restaurar pestañas al iniciar + CONTINUAR + ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty)? + HACER STAGE A TODO & COMMIT + ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty) o hacer stage a todo y después commit? Asistente de Commit Convencional Cambio Importante: Incidencia Cerrada: @@ -201,6 +224,7 @@ Introduzca el nombre de la rama. Los espacios serán reemplazados con guiones. Crear Rama Local + Sobrescribir la rama existente Crear Etiqueta... Nueva Etiqueta En: Firma GPG @@ -215,6 +239,9 @@ ligera Mantenga Ctrl para iniciar directamente Cortar + Desinicializar Submódulo + Forzar desinicialización incluso si contiene cambios locales. + Submódulo: Eliminar Rama Rama: ¡Estás a punto de eliminar una rama remota! @@ -250,6 +277,7 @@ Mostrar símbolos ocultos Diferencia Lado a Lado SUBMÓDULO + BORRADO NUEVO Intercambiar Resaltado de Sintaxis @@ -274,7 +302,6 @@ Editar Repositorio Seleccionado Ejecutar Acción Personalizada Nombre de la Acción: - Fast-Forward (sin checkout) Fetch Fetch todos los remotos Utilizar opción '--force' @@ -309,6 +336,8 @@ FLOW - Finalizar Hotfix FLOW - Finalizar Release Destino: + Push al/los remoto(s) después de Finalizar + Squash durante el merge Hotfix: Prefijo de Hotfix: Inicializar Git-Flow @@ -370,6 +399,8 @@ Ir a la página anterior Crear nueva página Abrir diálogo de preferencias + Cambiar espacio de trabajo activo + Cambiar página activa REPOSITORIO Commit cambios staged Commit y push cambios staged @@ -390,6 +421,7 @@ Cerrar panel de búsqueda Buscar siguiente coincidencia Buscar coincidencia anterior + Abrir con herramienta diff/merge externa Abrir panel de búsqueda Descartar Stage @@ -411,6 +443,8 @@ Abrir en el Navegador ERROR AVISO + Espacios de trabajo + Páginas Merge Rama En: Opción de Merge: @@ -444,8 +478,8 @@ Hace {0} meses Hace {0} años Ayer + Usa 'Shift+Enter' para introducir una nueva línea. 'Enter' es el atajo del botón OK Preferencias - Opciones Avanzadas OPEN AI Analizar Diff Prompt Clave API @@ -485,7 +519,8 @@ Email de usuario Email global del usuario git Habilitar --prune para fetch - Se requiere Git (>= 2.23.0) para esta aplicación + Habilitar --ignore-cr-at-eol en diff + Se requiere Git (>= 2.25.1) para esta aplicación Ruta de instalación Habilitar verificación HTTP SSL Nombre de usuario @@ -509,12 +544,11 @@ Podar información de worktree en `$GIT_COMMON_DIR/worktrees` Pull Rama Remota: - Fetch todas las ramas En: Cambios Locales: Descartar Stash & Reaplicar - Fetch sin etiquetas + Actualizar todos los submódulos Remoto: Pull (Fetch & Merge) Usar rebase en lugar de merge @@ -536,7 +570,6 @@ Stash & reaplicar cambios locales En: Rebase: - Refrescar Añadir Remoto Editar Remoto Nombre: @@ -558,13 +591,18 @@ Rama: ABORTAR Auto fetching cambios desde remotos... + Ordenar + Por Fecha de Committer + Por Nombre Limpiar (GC & Prune) Ejecutar comando `git gc` para este repositorio. Limpiar todo + Limpiar Configurar este repositorio CONTINUAR Acciones Personalizadas No hay ninguna Acción Personalizada + Descartar todos los cambios Habilitar Opción '--reflog' Abrir en el Explorador Buscar Ramas/Etiquetas/Submódulos @@ -592,10 +630,12 @@ Buscar Commit Autor Committer + Contenido Archivo Mensaje SHA Rama Actual + Mostrar Submódulos como Árbol Mostrar Etiquetas como Árbol OMITIR Estadísticas @@ -605,11 +645,12 @@ ETIQUETAS NUEVA ETIQUETA Por Fecha de Creación - Por Nombre (Ascendiente) - Por Nombre (Descendiente) + Por Nombre Ordenar Abrir en Terminal Usar tiempo relativo en las historias + Ver Logs + Visitar '{0}' en el Navegador WORKTREES AÑADIR WORKTREE PRUNE @@ -618,12 +659,14 @@ Modo de Reset: Mover a: Rama Actual: + Resetear Rama (Sin hacer Checkout) + Mover A: + Rama: Revelar en el Explorador de Archivos Revertir Commit Commit: Commit revertir cambios Reescribir Mensaje de Commit - Usa 'Shift+Enter' para introducir una nueva línea. 'Enter' es el atajo del botón OK Ejecutando. Por favor espera... GUARDAR Guardar Como... @@ -649,12 +692,9 @@ Ruta de almacenamiento de la clave privada SSH INICIAR Stash - Restaurar automáticamente después del stashing - Tus archivos de trabajo permanecen sin cambios, pero se guarda un stash. Incluir archivos no rastreados - Mantener archivos staged Mensaje: - Opcional. Nombre de este stash + Opcional. Información de este stash Solo cambios staged ¡Tanto los cambios staged como los no staged de los archivos seleccionados serán stashed! Stash Cambios Locales @@ -677,11 +717,18 @@ SUBMÓDULOS Añadir Submódulo Copiar Ruta Relativa + Desinicializar Submódulo Fetch submódulos anidados Abrir Repositorio del Submódulo Ruta Relativa: Carpeta relativa para almacenar este módulo. Eliminar Submódulo + ESTADO + modificado + no inicializado + revisión cambiada + unmerged + URL OK Copiar Nombre de la Etiqueta Copiar Mensaje de la Etiqueta @@ -695,6 +742,10 @@ Submódulo: Usar opción --remote URL: + Logs + LIMPIAR TODO + Copiar + Borrar Advertencia Página de Bienvenida Crear Grupo @@ -714,21 +765,26 @@ Git Ignore Ignorar todos los archivos *{0} Ignorar archivos *{0} en la misma carpeta - Ignorar archivos en la misma carpeta Ignorar solo este archivo Enmendar - Puedes stagear este archivo ahora. + Puedes hacer stage a este archivo ahora. COMMIT COMMIT & PUSH Plantilla/Historias Activar evento de clic Commit (Editar) - Stagear todos los cambios y commit + Hacer stage a todos los cambios y commit + Tienes {0} archivo(s) en stage, pero solo {1} archivo(s) mostrado(s) ({2} archivo(s) están filtrados). ¿Quieres continuar? CONFLICTOS DETECTADOS + ABRIR HERRAMIENTA DE MERGE EXTERNA + ABRIR TODOS LOS CONFLICTOS EN HERRAMIENTA DE MERGE EXTERNA LOS CONFLICTOS DE ARCHIVOS ESTÁN RESUELTOS + USAR MÍOS + USAR SUYOS INCLUIR ARCHIVOS NO RASTREADOS NO HAY MENSAJES DE ENTRADA RECIENTES NO HAY PLANTILLAS DE COMMIT + Restablecer Autor Haz clic derecho en el(los) archivo(s) seleccionado(s) y elige tu opción para resolver conflictos. Firmar STAGED @@ -737,7 +793,7 @@ UNSTAGED STAGE STAGE TODO - VER ASSUME UNCHANGED + VER ASSUME UNCHANGED Plantilla: ${0}$ ESPACIO DE TRABAJO: Configura Espacios de Trabajo... diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 83d590f5..8e1fd7bd 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -39,17 +39,17 @@ FICHIERS PRÉSUMÉS INCHANGÉS PAS DE FICHIERS PRÉSUMÉS INCHANGÉS SUPPRIMER + Rafraîchir FICHIER BINAIRE NON SUPPORTÉ !!! Blâme LE BLÂME SUR CE FICHIER N'EST PAS SUPPORTÉ!!! Récupérer ${0}$... - Comparer avec HEAD + Comparer avec ${0}$ Comparer avec le worktree Copier le nom de la branche Action personnalisée Supprimer ${0}$... Supprimer {0} branches sélectionnées - Rejeter tous les changements Fast-Forward vers ${0}$ Fetch ${0}$ vers ${1}$... Git Flow - Terminer ${0}$ @@ -86,7 +86,6 @@ Commit tous les changements Ligne principale : Habituellement, on ne peut pas cherry-pick un commit car on ne sait pas quel côté devrait être considéré comme principal. Cette option permet de rejouer les changements relatifs au parent spécifié. - Cherry Pick Supprimer les stashes Vous essayez de supprimer tous les stashes. Êtes-vous sûr de vouloir continuer ? Cloner repository distant @@ -104,8 +103,8 @@ Cherry-Pick ... Comparer avec HEAD Comparer avec le worktree - Copier les informations - Copier le SHA + Informations + SHA Action personnalisée Rebase interactif de ${0}$ ici Fusionner dans ${0}$ @@ -180,6 +179,7 @@ Nom d'utilisateur pour ce dépôt Espaces de travail Couleur + Nom Restaurer les onglets au démarrage Assistant Commits Conventionnels Changement Radical : @@ -275,7 +275,6 @@ Éditer le dépôt sélectionné Lancer action personnalisée Nom de l'action : - Fast-Forward (sans récupération) Fetch Fetch toutes les branches distantes Outrepasser les vérifications de refs @@ -445,6 +444,7 @@ il y a {0} mois il y a {0} ans Hier + Utiliser 'Maj+Entrée' pour insérer une nouvelle ligne. 'Entrée' est la touche pour valider Préférences IA Analyser Diff Prompt @@ -456,8 +456,6 @@ Activer le streaming APPARENCE Police par défaut - Taille de police par défaut - Taille de police de l'éditeur Largeur de tab dans l'éditeur Taille de police Défaut @@ -487,7 +485,7 @@ E-mail utilsateur E-mail utilsateur global Activer --prune pour fetch - Cette application requière Git (>= 2.23.0) + Cette application requière Git (>= 2.25.1) Chemin d'installation Activer la vérification HTTP SSL Nom d'utilisateur @@ -511,12 +509,10 @@ Élaguer les information de worktree dans `$GIT_COMMON_DIR/worktrees` Pull Branche distante : - Fetch toutes les branches Dans : Changements locaux : Rejeter Stash & Réappliquer - Fetch sans les tags Dépôt distant : Pull (Fetch & Merge) Utiliser rebase au lieu de merge @@ -538,7 +534,6 @@ Stash & réappliquer changements locaux Sur : Rebase : - Rafraîchir Ajouter dépôt distant Modifier dépôt distant Nom : @@ -567,6 +562,7 @@ CONTINUER Actions personnalisées Pas d'actions personnalisées + Rejeter tous les changements Activer l'option '--reflog' Ouvrir dans l'explorateur de fichiers Rechercher Branches/Tags/Submodules @@ -607,8 +603,7 @@ TAGS NOUVEAU TAG Par date de créateur - Par nom (Croissant) - Par nom (Décroissant) + Par nom Trier Ouvrir dans un terminal Utiliser le temps relatif dans les historiques @@ -625,7 +620,6 @@ Commit : Commit les changements de l'annulation Reformuler le message de commit - Utiliser 'Maj+Entrée' pour insérer une nouvelle ligne. 'Entrée' est la touche pour valider En exécution. Veuillez patienter... SAUVEGARDER Sauvegarder en tant que... @@ -651,12 +645,9 @@ Chemin du magasin de clés privées SSH START Stash - Auto-restauration après le stash - Vos fichiers de travail restent inchangés, mais une sauvegarde est enregistrée. Inclure les fichiers non-suivis - Garder les fichiers indexés Message : - Optionnel. Nom de ce stash + Optionnel. Information de ce stash Seulement les changements indexés Les modifications indexées et non-indexées des fichiers sélectionnés seront stockées!!! Stash les changements locaux @@ -716,7 +707,6 @@ Git Ignore Ignorer tous les *{0} fichiers Ignorer *{0} fichiers dans le même dossier - Ignorer les fichiers dans le même dossier N'ignorer que ce fichier Amender Vous pouvez indexer ce fichier. @@ -739,7 +729,7 @@ NON INDEXÉ INDEXER INDEXER TOUT - VOIR LES FICHIERS PRÉSUMÉS INCHANGÉS + VOIR LES FICHIERS PRÉSUMÉS INCHANGÉS Modèle: ${0}$ ESPACE DE TRAVAIL : Configurer les espaces de travail... diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 59bf3028..62f15e38 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -39,17 +39,24 @@ FILE ASSUNTI COME INVARIATI NESSUN FILE ASSUNTO COME INVARIATO RIMUOVI + Aggiorna FILE BINARIO NON SUPPORTATO!!! + Biseca + Annulla + Cattiva + Bisecando. La HEAD corrente è buona o cattiva? + Buona + Salta + Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. Attribuisci L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!! Checkout ${0}$... - Confronta con HEAD + Confronta con ${0}$ Confronta con Worktree Copia Nome Branch Azione personalizzata Elimina ${0}$... Elimina i {0} branch selezionati - Scarta tutte le modifiche Avanzamento Veloce a ${0}$ Recupera ${0}$ in ${1}$... Git Flow - Completa ${0}$ @@ -79,6 +86,7 @@ Modifiche Locali: Scarta Stasha e Ripristina + Aggiorna tutti i sottomoduli Branch: Cherry Pick Aggiungi sorgente al messaggio di commit @@ -103,8 +111,11 @@ Cherry-Pick... Confronta con HEAD Confronta con Worktree - Copia Info - Copia SHA + Autore + Committer + Informazioni + SHA + Oggetto Azione Personalizzata Riallinea Interattivamente ${0}$ fino a Qui Unisci a ${0}$ @@ -136,6 +147,7 @@ SHA Apri nel Browser Descrizione + OGGETTO Inserisci l'oggetto del commit Configura Repository TEMPLATE DI COMMIT @@ -157,6 +169,7 @@ Recupera automaticamente i remoti Minuto/i Remoto Predefinito + Modalità di Merge Preferita TRACCIAMENTO ISSUE Aggiungi una regola di esempio per Azure DevOps Aggiungi una regola di esempio per un Issue Gitee @@ -179,7 +192,12 @@ Nome utente per questo repository Spazi di Lavoro Colore + Nome Ripristina schede all'avvio + CONTINUA + Trovato un commit vuoto! Vuoi procedere (--allow-empty)? + STAGE DI TUTTO E COMMITTA + Trovato un commit vuoto! Vuoi procedere (--allow-empty) o fare lo stage di tutto e committare? Guida Commit Convenzionali Modifica Sostanziale: Issue Chiusa: @@ -189,6 +207,7 @@ Tipo di Modifica: Copia Copia Tutto il Testo + Copia Intero Percorso Copia Percorso Crea Branch... Basato Su: @@ -273,7 +292,6 @@ Modifica Repository Selezionato Esegui Azione Personalizzata Nome Azione: - Avanzamento Veloce (senza verifica) Recupera Recupera da tutti i remoti Forza la sovrascrittura dei riferimenti locali @@ -300,7 +318,6 @@ Cronologia File MODIFICA CONTENUTO - FILTRO Git-Flow Branch di Sviluppo: Feature: @@ -309,6 +326,8 @@ FLOW - Completa Hotfix FLOW - Completa Rilascio Target: + Invia al remote dopo aver finito + Esegui squash durante il merge Hotfix: Prefisso Hotfix: Inizializza Git-Flow @@ -390,6 +409,7 @@ 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 @@ -444,6 +464,7 @@ {0} mesi fa {0} anni fa Ieri + Usa 'Shift+Enter' per inserire una nuova riga. 'Enter' è il tasto rapido per il pulsante OK Preferenze AI Analizza il Prompt Differenza @@ -476,6 +497,7 @@ Numero massimo di commit nella cronologia Mostra nel grafico l'orario dell'autore anziché quello del commit Mostra i figli nei dettagli del commit + Mostra i tag nel grafico dei commit Lunghezza Guida Oggetto GIT Abilita Auto CRLF @@ -483,7 +505,8 @@ Email Utente Email utente Git globale Abilita --prune durante il fetch - Questa applicazione richiede Git (>= 2.23.0) + Abilita --ignore-cr-at-eol nel diff + Questa applicazione richiede Git (>= 2.25.1) Percorso Installazione Abilita la verifica HTTP SSL Nome Utente @@ -507,12 +530,10 @@ Potatura delle informazioni di worktree in `$GIT_COMMON_DIR/worktrees` Scarica Branch Remoto: - Recupera tutti i branch In: Modifiche Locali: Scarta Stasha e Riapplica - Recupera senza tag Remoto: Scarica (Recupera e Unisci) Riallineare anziché unire @@ -534,7 +555,6 @@ Stasha e Riapplica modifiche locali Su: Riallinea: - Aggiorna Aggiungi Remoto Modifica Remoto Nome: @@ -556,6 +576,9 @@ Branch: ANNULLA Recupero automatico delle modifiche dai remoti... + Ordina + Per data del committer + Per nome Pulizia (GC e Potatura) Esegui il comando `git gc` per questo repository. Cancella tutto @@ -563,10 +586,10 @@ CONTINUA Azioni Personalizzate Nessuna Azione Personalizzata + Scarta tutte le modifiche Abilita opzione '--reflog' Apri nell'Esplora File Cerca Branch/Tag/Sottomodulo - FILTRATO DA: Visibilità nel grafico Non impostato Nascondi nel grafico dei commit @@ -590,11 +613,13 @@ AGGIUNGI REMOTO Cerca Commit Autore - Committente + Committer + Contenuto File Messaggio SHA Branch Corrente + Mostra i Sottomoduli Come Albero Mostra Tag come Albero SALTA Statistiche @@ -604,11 +629,12 @@ TAG NUOVO TAG Per data di creazione - Per nome (ascendente) - Per nome (discendente) + Per nome Ordina Apri nel Terminale Usa tempo relativo nello storico + Visualizza i Log + Visita '{0}' nel Browser WORKTREE AGGIUNGI WORKTREE POTATURA @@ -622,7 +648,6 @@ Commit: Commit delle modifiche di ripristino Modifica Messaggio di Commit - Usa 'Shift+Enter' per inserire una nuova riga. 'Enter' è il tasto rapido per il pulsante OK In esecuzione. Attendere... SALVA Salva come... @@ -648,12 +673,9 @@ Percorso per la chiave SSH privata AVVIA Stasha - Auto-ripristino dopo lo stash - I tuoi file di lavoro rimangono inalterati, ma viene salvato uno stash. Includi file non tracciati - Mantieni file in stage Messaggio: - Opzionale. Nome di questo stash + Opzionale. Informazioni di questo stash Solo modifiche in stage Sia le modifiche in stage che quelle non in stage dei file selezionati saranno stashate!!! Stasha Modifiche Locali @@ -681,6 +703,12 @@ Percorso Relativo: Cartella relativa per memorizzare questo modulo. Elimina Sottomodulo + STATO + modificato + non inizializzato + revisione cambiata + non unito + URL OK Copia Nome Tag Copia Messaggio Tag @@ -694,6 +722,10 @@ Sottomodulo: Usa opzione --remote URL: + Log + CANCELLA TUTTO + Copia + Elimina Avviso Pagina di Benvenuto Crea Gruppo @@ -713,7 +745,6 @@ Git Ignore Ignora tutti i file *{0} Ignora i file *{0} nella stessa cartella - Ignora i file nella stessa cartella Ignora solo questo file Modifica Puoi aggiungere in stage questo file ora. @@ -723,8 +754,13 @@ Attiva evento click Commit (Modifica) Stage di tutte le modifiche e fai il commit + Hai stageato {0} file ma solo {1} file mostrati ({2} file sono stati filtrati). Vuoi procedere? CONFLITTI RILEVATI + APRI STRUMENTO DI MERGE ESTERNO + APRI TUTTI I CONFLITTI NELLO STRUMENTO DI MERGE ESTERNO CONFLITTI NEI FILE RISOLTI + USO IL MIO + USO IL LORO INCLUDI FILE NON TRACCIATI NESSUN MESSAGGIO RECENTE INSERITO NESSUN TEMPLATE DI COMMIT @@ -736,7 +772,7 @@ NON IN STAGE FAI LO STAGE FAI LO STAGE DI TUTTO - VISUALIZZA COME NON MODIFICATO + VISUALIZZA COME NON MODIFICATO Template: ${0}$ WORKSPACE: Configura Workspaces... diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index fe643b5e..bf9fe586 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -39,17 +39,16 @@ 変更されていないとみなされるファイル 変更されていないとみなされるファイルはありません 削除 + 更新 バイナリファイルはサポートされていません!!! Blame BLAMEではこのファイルはサポートされていません!!! ${0}$ をチェックアウトする... - HEADと比較 ワークツリーと比較 ブランチ名をコピー カスタムアクション ${0}$を削除... 選択中の{0}個のブランチを削除 - すべての変更を破棄 ${0}$ へ早送りする ${0}$ から ${1}$ へフェッチする Git Flow - Finish ${0}$ @@ -103,8 +102,8 @@ チェリーピック... HEADと比較 ワークツリーと比較 - 情報をコピー - SHAをコピー + 情報 + SHA カスタムアクション ${0}$ ブランチをここにインタラクティブリベース ${0}$ にマージ @@ -179,6 +178,7 @@ このリポジトリにおけるユーザー名 ワークスペース + 名前 起動時にタブを復元 Conventional Commitヘルパー 破壊的変更: @@ -274,7 +274,6 @@ 選択中のリポジトリを編集 カスタムアクションを実行 アクション名: - (チェックアウトせずに)ブランチを早送りする フェッチ すべてのリモートをフェッチ ローカル参照を強制的に上書き @@ -444,6 +443,7 @@ {0} ヶ月前 {0} 年前 昨日 + 改行には'Shift+Enter'キーを使用します。 'Enter"はOKボタンのホットキーとして機能します。 設定 AI 差分分析プロンプト @@ -484,7 +484,7 @@ ユーザー Eメールアドレス グローバルgitのEメールアドレス フェッチ時に--pruneを有効化 - Git (>= 2.23.0) はこのアプリで必要です + Git (>= 2.25.1) はこのアプリで必要です インストール パス HTTP SSL 検証を有効にする ユーザー名 @@ -508,12 +508,10 @@ `$GIT_DIR/worktrees` の作業ツリー情報を削除 プル ブランチ: - すべてのブランチをフェッチ 宛先: ローカルの変更: 破棄 スタッシュして再適用 - タグなしでフェッチ リモート: プル (フェッチ & マージ) マージの代わりにリベースを使用 @@ -535,7 +533,6 @@ ローカルの変更をスタッシュして再適用 On: リベース: - 更新 リモートを追加 リモートを編集 名前: @@ -564,6 +561,7 @@ 続ける カスタムアクション カスタムアクションがありません + すべての変更を破棄 `--reflog` オプションを有効化 ファイルブラウザーで開く ブランチ/タグ/サブモジュールを検索 @@ -603,8 +601,7 @@ タグ 新しいタグを作成 作成者日時 - 名前 (昇順) - 名前 (降順) + 名前 ソート ターミナルで開く 履歴に相対時間を使用 @@ -621,7 +618,6 @@ コミット: コミットの変更を戻す コミットメッセージを書き直す - 改行には'Shift+Enter'キーを使用します。 'Enter"はOKボタンのホットキーとして機能します。 実行中です。しばらくお待ちください... 保存 名前を付けて保存... @@ -647,12 +643,9 @@ プライベートSSHキーストアのパス スタート スタッシュ - スタッシュ後に自動で復元 - 作業ファイルは変更されず、スタッシュが保存されます。 追跡されていないファイルを含める - ステージされたファイルを保持 メッセージ: - オプション. このスタッシュの名前を入力 + オプション. このスタッシュの情報 ステージされた変更のみ 選択したファイルの、ステージされた変更とステージされていない変更の両方がスタッシュされます!!! ローカルの変更をスタッシュ @@ -712,7 +705,6 @@ Git Ignore すべての*{0}ファイルを無視 同じフォルダ内の*{0}ファイルを無視 - 同じフォルダ内のファイルを無視 このファイルのみを無視 Amend このファイルを今すぐステージできます。 @@ -735,7 +727,7 @@ 未ステージのファイル ステージへ移動 すべてステージへ移動 - 変更されていないとみなしたものを表示 + 変更されていないとみなしたものを表示 テンプレート: ${0}$ ワークスペース: ワークスペースを設定... diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 3caac3cc..fecd9540 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -33,16 +33,16 @@ ARQUIVOS CONSIDERADOS SEM ALTERAÇÕES NENHUM ARQUIVO CONSIDERADO SEM ALTERAÇÕES REMOVER + Atualizar ARQUIVO BINÁRIO NÃO SUPORTADO!!! Blame BLAME NESTE ARQUIVO NÃO É SUPORTADO!!! Checkout ${0}$... - Comparar com HEAD + Comparar com ${0}$ Comparar com Worktree Copiar Nome do Branch Excluir ${0}$... Excluir {0} branches selecionados - Descartar todas as alterações Fast-Forward para ${0}$ Buscar ${0}$ em ${1}$... Git Flow - Finalizar ${0}$ @@ -93,8 +93,8 @@ Cherry-Pick ... Comparar com HEAD Comparar com Worktree - Copiar Informações - Copiar SHA + Informações + SHA Ação customizada Rebase Interativo ${0}$ até Aqui Rebase ${0}$ até Aqui @@ -161,6 +161,7 @@ Nome de usuário para este repositório Workspaces Cor + Nome Restaurar abas ao inicializar Assistente de Conventional Commit Breaking Change: @@ -248,7 +249,6 @@ Editar Repositório Selecionado Executar ação customizada Nome da ação: - Fast-Forward (sem checkout) Buscar Buscar todos os remotos Buscar sem tags @@ -406,6 +406,7 @@ {0} meses atrás {0} anos atrás Ontem + Use 'Shift+Enter' para inserir uma nova linha. 'Enter' é a tecla de atalho do botão OK Preferências INTELIGÊNCIA ARTIFICIAL Prompt para Analisar Diff @@ -441,7 +442,7 @@ Email do Usuário Email global do usuário git Habilita --prune ao buscar - Git (>= 2.23.0) é necessário para este aplicativo + Git (>= 2.25.1) é necessário para este aplicativo Caminho de Instalação Nome do Usuário Nome global do usuário git @@ -464,12 +465,10 @@ Podar informações de worktree em `$GIT_COMMON_DIR/worktrees` Puxar Branch Remoto: - Buscar todos os branches Para: Alterações Locais: Descartar Guardar & Reaplicar - Buscar sem tags Remoto: Puxar (Buscar & Mesclar) Usar rebase em vez de merge @@ -491,7 +490,6 @@ Guardar & reaplicar alterações locais Em: Rebase: - Atualizar Adicionar Remoto Editar Remoto Nome: @@ -520,6 +518,7 @@ CONTINUAR Ações customizada Nenhuma ação customizada + Descartar todas as alterações Habilitar opção '--reflog' Abrir no Navegador de Arquivos Pesquisar Branches/Tags/Submódulos @@ -565,7 +564,6 @@ Commit: Commitar alterações de reversão Reescrever Mensagem do Commit - Use 'Shift+Enter' para inserir uma nova linha. 'Enter' é a tecla de atalho do botão OK Executando. Por favor, aguarde... SALVAR Salvar Como... @@ -587,9 +585,8 @@ INICIAR Stash Incluir arquivos não rastreados - Manter arquivos em stage Mensagem: - Opcional. Nome deste stash + Opcional. Informações deste stash Apenas mudanças em stage Tanto mudanças em stage e fora de stage dos arquivos selecionados serão enviadas para stash!!! Guardar Alterações Locais @@ -648,7 +645,6 @@ Git Ignore Ignorar todos os arquivos *{0} Ignorar arquivos *{0} na mesma pasta - Ignorar arquivos na mesma pasta Ignorar apenas este arquivo Corrigir Você pode stagear este arquivo agora. @@ -669,7 +665,7 @@ UNSTAGED STAGE STAGE TODOS - VER SUPOR NÃO ALTERADO + VER SUPOR NÃO ALTERADO Template: ${0}$ Workspaces: Configurar workspaces... diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index ee04f4d1..708ff7cc 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -14,8 +14,8 @@ Отслеживание ветки: Отслеживание внешней ветки Переключиться на: - создать новую ветку - ветку из списка + Создать новую ветку + Ветку из списка Помощник OpenAI ПЕРЕСОЗДАТЬ Использовать OpenAI для создания сообщения о ревизии @@ -39,27 +39,36 @@ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ СПИСОК ПУСТ УДАЛИТЬ + Загрузить картинку... + Обновить ДВОИЧНЫЙ ФАЙЛ НЕ ПОДДЕРЖИВАЕТСЯ!!! + Раздвоить + О + Плохая + Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая? + Хорошая + Пропустить + Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой. Расследование РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! - Проверить ${0}$... - Сравнить с ГОЛОВОЙ (HEAD) + Переключиться на ${0}$... + Сравнить с ${0}$ Сравнить с рабочим каталогом Копировать имя ветки Изменить действие Удалить ${0}$... Удалить выбранные {0} ветки - Отклонить все изменения. Перемотать вперёд к ${0}$ Извлечь ${0}$ в ${1}$... - Поток Git - Завершение ${0}$ + Git-процесс - Завершение ${0}$ Влить ${0}$ в ${1}$... Влить {0} выделенных веток в текущую - Забрать ${0}$ - Забрать ${0}$ в ${1}$... + Загрузить ${0}$ + Загрузить ${0}$ в ${1}$... Выложить ${0}$ Переместить ${0}$ на ${1}$... Переименовать ${0}$... + Сбросить ${0}$ к ${1}$... Отслеживать ветку... Сравнение веток Недопустимая основная ветка! @@ -79,7 +88,10 @@ Локальные изменения: Отклонить Отложить и примненить повторно + Обновить все подкаталоги Ветка: + Переключиться и перемотать + Перемотать к: Частичный выбор Добавить источник для ревизии сообщения Ревизия(и): @@ -103,12 +115,16 @@ Применить несколько ревизий ... Сравнить c ГОЛОВОЙ (HEAD) Сравнить с рабочим каталогом - Копировать информацию - Копировать SHA + Автор + Ревизор + Информацию + SHA + Субъект Пользовательское действие Интерактивное перемещение (rebase -i) ${0}$ сюда Влить в ${0}$ Влить ... + Выложить ${0}$ в ${1}$ Переместить ${0}$ сюда Сбросить ${0}$ сюда Отменить ревизию @@ -117,6 +133,7 @@ Объединить с предыдущей ревизией Объединить все следующие ревизии с этим ИЗМЕНЕНИЯ + изменённый(х) файл(ов) Найти изменения.... ФАЙЛЫ Файл LFS @@ -136,6 +153,7 @@ SHA Открыть в браузере Описание + СУБЪЕКТ Введите тему ревизии Настройка репозитория ШАБЛОН РЕВИЗИИ @@ -184,8 +202,8 @@ Восстанавливать вкладки при запуске ПРОДОЛЖИТЬ Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty)? - Подготовить все и зафиксировать ревизию - Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить все, затем зафиксировать ревизию? + Сформировать всё и зафиксировать ревизию + Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, затем зафиксировать ревизию? Общепринятый помощник по ревизии Кардинальные изменения: Закрытая тема: @@ -199,7 +217,7 @@ Копировать путь Создать ветку... Основан на: - Проверить созданную ветку + Переключиться на созданную ветку Локальные изменения: Отклонить Отложить и применить повторно @@ -207,10 +225,11 @@ Введите имя ветки. Пробелы будут заменены на тире. Создать локальную ветку + Перезаписать существующую ветку Создать метку... Новая метка у: GPG подпись - Сообщение с меткой: + Сообщение с меткой: Необязательно. Имя метки: Рекомендуемый формат: v1.0.0-alpha @@ -221,6 +240,9 @@ Простой Удерживайте Ctrl, чтобы сразу начать Вырезать + Удалить подмодуль + Принудительно удалить даже если содержит локальные изменения. + Подмодуль: Удалить ветку Ветка: Вы собираетесь удалить внешнюю ветку!!! @@ -240,22 +262,23 @@ Удалить метку Метка: Удалить из внешнего репозитория - РАЗНИЦА БИНАРНИКОВ + СРАВНЕНИЕ БИНАРНИКОВ НОВЫЙ СТАРЫЙ Копировать Режим файла изменён - Первое различие - Игнорировать изменение пробелов - Последнее различие + Первое сравнение + Игнорировать изменения пробелов + Последнее сравнение ИЗМЕНЕНИЕ ОБЪЕКТА LFS - Следующее различие + Следующее сравнение НИКАКИХ ИЗМЕНЕНИЙ ИЛИ МЕНЯЕТСЯ ТОЛЬКО EOL - Предыдущее различие + Предыдущее сравнение Сохранить как заплатку Показывать скрытые символы - Различие рядом + Сравнение рядом ПОДМОДУЛЬ + УДАЛЁН НОВЫЙ Обмен Подсветка синтаксиса @@ -280,7 +303,6 @@ Редактировать выбранный репозиторий Выполнить пользовательское действие Имя действия: - Быстрая перемотка вперёд (без проверки) Извлечь Извлечь все внешние репозитории Разрешить опцию (--force) @@ -294,41 +316,43 @@ Открыть расширенный инструмент слияния Взять версию ${0}$ Сохранить как файл заплатки... - Подготовить - Подготовленные {0} файлы - Подготовленные изменения в выбранной(ых) строке(ах) + Сформировать + Сформированные {0} файлы + Сформированные изменения в выбранной(ых) строке(ах) Отложить... Отложить {0} файлов... - Снять подготовленный - Неподготовленные {0} файлы - Неподготовленные изменения в выбранной(ых) строке(ах) + Расформировать + Несформированные {0} файлы + Несформированные изменения в выбранной(ых) строке(ах) Использовать мой (checkout --ours) Использовать их (checkout --theirs) История файлов ИЗМЕНИТЬ СОДЕРЖИМОЕ - Git-поток + Git-процесс Ветка разработчика: Свойство: Свойство префикса: - ПОТОК - Свойства завершения - ПОТОК - Закончить исправление - ПОТОК - Завершить выпуск + ПРОЦЕСС - Свойства завершения + ПРОЦЕСС - Закончить исправление + ПРОЦЕСС - Завершить выпуск Цель: + Выложить на удалённый(ые) после завершения + Втиснуть при слиянии Исправление: Префикс исправлений: - Создать Git-поток + Создать Git-процесс Держать ветку Производственная ветка: Выпуск: Префикс выпуска: Свойство запуска... - ПОТОК - Свойство запуска + ПРОЦЕСС - Свойство запуска Запуск исправлений... - ПОТОК - Запуск исправлений + ПРОЦЕСС - Запуск исправлений Ввести имя Запуск выпуска... - ПОТОК - Запуск выпуска + ПРОЦЕСС - Запуск выпуска Префикс метки версии: Git LFS (хранилище больших файлов) Добавить шаблон отслеживания... @@ -348,9 +372,9 @@ Принудительно разблокировать Обрезать Запустить (git lfs prune), чтобы удалить старые файлы LFS из локального хранилища - Забрать + Загрузить Запустить (git lfs pull), чтобы загрузить все файлы LFS Git для текущей ссылки и проверить - Забрать объекты LFS + Загрузить объекты LFS Выложить Отправляйте большие файлы, помещенные в очередь, в конечную точку LFS Git Выложить объекты LFS @@ -376,19 +400,21 @@ Перейти на предыдущую вкладку Создать новую вкладку Открыть диалоговое окно настроек + Переключить активное рабочее место + Переключить активную страницу РЕПОЗИТОРИЙ - Зафиксировать подготовленные изменения - Зафиксировать и выложить подготовленные изменения - Подготовить все изменения и зафиксировать + Зафиксировать сформированные изменения + Зафиксировать и выложить сформированные изменения + Сформировать все изменения и зафиксировать Создать новую ветку на основе выбранной ветки Отклонить выбранные изменения Извлечение, запускается сразу Режим доски (по умолчанию) Режим поиска ревизий - Забрать, запускается сразу + Загрузить, запускается сразу Выложить, запускается сразу Принудительно перезагрузить репозиторий - Подготовленные/Неподготовленные выбранные изменения + Сформированные/Несформированные выбранные изменения Переключить на «Изменения» Переключить на «Истории» Переключить на «Отложенные» @@ -396,10 +422,11 @@ Закрыть панель поиска Найти следующее совпадение Найти предыдущее совпадение + Открыть с внешним инструментом сравнения/слияние Открыть панель поиска Отклонить - Подготовить - Снять из подготовленных + Сформировать + Расформировать Создать репозиторий Путь: Выполняется частичный перенос ревизий (cherry-pick). @@ -417,7 +444,10 @@ Открыть в браузере ОШИБКА УВЕДОМЛЕНИЕ + Рабочие места + Страницы Влить ветку + Изменить сообщение слияния В: Опции слияния: Источник: @@ -450,11 +480,12 @@ {0} месяцев назад {0} лет назад Вчера + Используйте «Shift+Enter» для ввода новой строки. «Enter» - это горячая клавиша кнопки «OK» Параметры ОТКРЫТЬ ИИ - Запрос на анализ различий + Запрос на анализ сравнения Ключ API - Произвести запрос на тему + Создать запрос на тему Модель Имя: Сервер @@ -471,9 +502,9 @@ Переопределение темы Использовать фиксированную ширину табуляции в строке заголовка. Использовать системное окно - ИНСТРУМЕНТ РАЗЛИЧИЙ/СЛИЯНИЯ + ИНСТРУМЕНТ СРАВНЕНИЙ/СЛИЯНИЯ Путь установки - Введите путь для инструмента различия/слияния + Введите путь для инструмента сравнения/слияния Инструмент ОСНОВНЫЕ Проверить обновления при старте @@ -490,7 +521,8 @@ Электроная почта пользователя Общая электроная почта пользователя git Разрешить (--prune) при скачивании - Для работы программы требуется версия Git (>= 2.23.0) + Разрешить (--ignore-cr-at-eol) в сравнении + Для работы программы требуется версия Git (>= 2.25.1) Путь установки Разрешить верификацию HTTP SSL Имя пользователя @@ -512,22 +544,23 @@ Цель: Удалить рабочий каталог Информация об обрезке рабочего каталога в «$GIT_COMMON_DIR/worktrees» - Забрать + Загрузить Ветка внешнего репозитория: - Извлечь все ветки В: Локальные изменения: Отклонить Отложить и применить повторно - Забрать без меток + Обновить все подмодули Внешний репозиторий: - Забрать (Получить и слить) + Загрузить (Получить и слить) Использовать перемещение вместо слияния Выложить Убедитесь, что подмодули были вставлены Принудительно выложить Локальная ветка: Внешний репозиторий: + Ревизия: + Выложить ревизию на удалёную Выложить изменения на внешний репозиторий Ветка внешнего репозитория: Отслеживать ветку @@ -541,7 +574,6 @@ Отложить и применить повторно локальные изменения На: Переместить: - Обновить Добавить внешний репозиторий Редактировать внешний репозиторий Имя: @@ -563,13 +595,18 @@ Ветка: Отказ Автоматическое извлечение изменений с внешних репозиторий... + Сортировать + По дате ревизора (исполнителя) + По имени Очистить (Сбор мусора и удаление) Запустить команду (git gc) для данного репозитория. Очистить всё + Очистить Настройка репозитория ПРОДОЛЖИТЬ Изменить действия Не изменять действия + Отклонить все изменения. Разрешить опцию --reflog Открыть в файловом менеджере Поиск веток, меток и подмодулей @@ -577,7 +614,6 @@ Не установлен (по умолчанию) Скрыть в графе ревизии Фильтр в графе ревизии - ОТФИЛЬТРОВАНО: Включить опцию (--first-parent) РАСПОЛОЖЕНИЕ Горизонтально @@ -598,10 +634,12 @@ Поиск ревизии Автор Ревизор + Содержимое Файл Сообщение SHA Текущая ветка + Показывать подмодули как дерево Показывать метки как катлог ПРОПУСТИТЬ Статистикa @@ -611,11 +649,12 @@ МЕТКИ НОВАЯ МЕТКА По дате создания - По имени (по возрастанию) - По имени (по убыванию) + По имени Сортировать Открыть в терминале Использовать относительное время в историях + Просмотр журналов + Посетить '{0}' в браузере РАБОЧИЕ КАТАЛОГИ ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ ОБРЕЗАТЬ @@ -624,12 +663,14 @@ Режим сброса: Переместить в: Текущая ветка: + Сброс ветки (без переключения) + Переместить в: + Ветка: Открыть в файловом менеджере Отменить ревизию Ревизия: Отмена ревизии Изменить комментарий ревизии - Используйте «Shift+Enter» для ввода новой строки. «Enter» - это горячая клавиша кнопки «OK» Запуск. Подождите пожалуйста... СОХРАНИТЬ Сохранить как... @@ -655,16 +696,15 @@ Путь хранения приватного ключа SSH ЗАПУСК Отложить - Автоматически восстанавливать после откладывания - Ваши рабочие файлы остаются неизменными, но отложенные сохранятся. Включить неотслеживаемые файлы - Хранить отложенные файлы Сообщение: Имя тайника (необязательно) - Только подготовленные изменения - Подготовленные так и неподготовленные изменения выбранных файлов будут сохранены!!! + Режим: + Только сформированные изменения + Сформированные так и несформированные изменения выбранных файлов будут сохранены!!! Отложить локальные изменения Принять + Копировать сообщение Отбросить Сохранить как заплатку... Отбросить тайник @@ -683,11 +723,18 @@ ПОДМОДУЛИ Добавить подмодули Копировать относительный путь + Удалить подмодуль Извлечение вложенных подмодулей Открыть подмодуль репозитория Каталог: Относительный путь для хранения подмодуля. Удалить подмодуль + СОСТОЯНИЕ + изменён + не создан + ревизия изменена + не слита + URL-адрес ОК Копировать имя метки Копировать сообщение с метки @@ -701,6 +748,10 @@ Подмодуль: Использовать опцию (--remote) Сетевой адрес: + Журналы + ОЧИСТИТЬ ВСЁ + Копировать + Удалить Предупреждение Приветствие Создать группу @@ -720,17 +771,17 @@ Игнорировать Git Игнорировать все *{0} файлы Игнорировать *{0} файлы в том же каталоге - Игнорировать файлы в том же каталоге + Игнорировать неотслеживаемые файлы в этом каталоге Игнорировать только эти файлы Изменить - Теперь вы можете подготовить этот файл. + Теперь вы можете сформировать этот файл. ЗАФИКСИРОВАТЬ ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории Запустить событие щелчка Зафиксировать (Редактировать) - Подготовить все изменения и зафиксировать - Вы подготовили {0} файл(ов), но отображается только {1} файл(ов) ({2} файл(ов) отфильтровано). Вы хотите продолжить? + Сформировать все изменения и зафиксировать + Вы сформировали {0} файл(ов), но отображается только {1} файл(ов) ({2} файл(ов) отфильтровано). Вы хотите продолжить? ОБНАРУЖЕНЫ КОНФЛИКТЫ ОТКРЫТЬ ВНЕШНИЙ ИНСТРУМЕНТ СЛИЯНИЯ ОТКРЫТЬ ВСЕ КОНФЛИКТЫ ВО ВНЕШНЕМ ИНСТРУМЕНТЕ СЛИЯНИЯ @@ -740,15 +791,16 @@ ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ НЕТ ШАБЛОНОВ РЕВИЗИИ + Сбросить автора Щёлкните правой кнопкой мыши выбранный файл(ы) и разрешите конфликты. Завершение работы - ПОДГОТОВЛЕННЫЕ - СНЯТЬ ПОДГОТОВЛЕННЫЙ - СНЯТЬ ВСЕ ПОДГОТОВЛЕННЫЕ - НЕПОДГОТОВЛЕННЫЕ - ПОДГОТОВИТЬ - ВСЕ ПОДГОТОВИТЬ - ОТКРЫТЬ СПИСОК НЕОТСЛЕЖИВАЕМЫХ ФАЙЛОВ + СФОРМИРОВАННЫЕ + РАСФОРМИРОВАТЬ + РАСФОРМИРОВАТЬ ВСЁ + НЕСФОРМИРОВАННЫЕ + СФОРМИРОВАТЬ + СФОРМИРОВАТЬ ВСЁ + ОТКРЫТЬ СПИСОК НЕОТСЛЕЖИВАЕМЫХ ФАЙЛОВ Шаблон: ${0}$ РАБОЧЕЕ ПРОСТРАНСТВО: Настройка рабочего пространства... diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 946a8d9c..248d5f42 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -39,17 +39,16 @@ கோப்புகள் மாற்றப்படவில்லை எனக் கருதப்படுகிறது எந்த கோப்புகளும் மாற்றப்படவில்லை எனக் கருதப்படுகிறது நீக்கு + புதுப்பி இருமம் கோப்பு ஆதரிக்கப்படவில்லை!!! குற்றச்சாட்டு இந்த கோப்பில் குற்றம் சாட்ட ஆதரிக்கப்படவில்லை!!! ${0}$ சரிபார்... - தலையுடன் ஒப்பிடுக பணிமரத்துடன் ஒப்பிடுக கிளை பெயரை நகலெடு தனிப்பயன் செயல் ${0}$ ஐ நீக்கு... தேர்ந்தெடுக்கப்பட்ட {0} கிளைகளை நீக்கு - எல்லா மாற்றங்களையும் நிராகரி ${0}$ இதற்கு வேகமாக முன்னோக்கிச் செல் ${0}$ ஐ ${1}$இல் பெறு... அறிவிலி ஓட்டம் - முடி ${0}$ @@ -103,8 +102,8 @@ கனி-பறி ... தலையுடன் ஒப்பிடுக பணிமரத்துடன் ஒப்பிடுக - தகவலை நகலெடு - பாகொவ-வை நகலெடு + தகவலை + பாகொவ-வை தனிப்பயன் செயல் இங்கே ${0}$ ஐ ஊடாடும் வகையில் மறுதளம் ${0}$ இதற்கு ஒன்றிணை @@ -179,6 +178,7 @@ இந்த களஞ்சியத்திற்கான பயனர் பெயர் பணியிடங்கள் நிறம் + பெயர் தாவல்களை மீட்டமை வழக்கமான உறுதிமொழி உதவியாளர் உடைக்கும் மாற்றம்: @@ -274,7 +274,6 @@ தேர்ந்தெடுக்கப்பட்ட களஞ்சியத்தைத் திருத்து தனிப்பயன் செயலை இயக்கு செயல் பெயர்: - வேகமாக முன்னோக்கி (சரிபார்க்காமல்) பெறு எல்லா தொலைகளையும் பெறு உள்ளக குறிப்புகளை கட்டாயமாக மீறு @@ -444,6 +443,7 @@ {0} திங்களுக்கு முன்பு {0} ஆண்டுகளுக்கு முன்பு நேற்று + புதிய வரியை உள்ளிட 'உயர்த்து+நுழை' ஐப் பயன்படுத்தவும். 'நுழை' என்பது சரி பொத்தானின் சூடானவிசை ஆகும் விருப்பத்தேர்வுகள் செநு வேறுபாடு உடனடியாக பகுப்பாய்வு செய் @@ -465,8 +465,8 @@ கருப்பொருள் மேலெழுதப்படுகிறது தலைப்புப்பட்டியில் நிலையான தாவல் அகலத்தைப் பயன்படுத்து சொந்த சாளர சட்டத்தைப் பயன்படுத்து - நிறுவல் பாதை வேறு/ஒன்றிணை கருவி + நிறுவல் பாதை வேறு/ஒன்றிணை கருவிக்கான பாதை உள்ளிடு கருவி பொது @@ -484,7 +484,7 @@ பயனர் மின்னஞ்சல் உலகளாவிய அறிவிலி பயனர் மின்னஞ்சல் --prune எடுக்கும்போது இயக்கு - அறிவிலி (>= 2.23.0) இந்த பயன்பாட்டிற்கு தேவைப்படுகிறது + அறிவிலி (>= 2.25.1) இந்த பயன்பாட்டிற்கு தேவைப்படுகிறது நிறுவல் பாதை உஉபநெ பாகுஅ சரிபார்ப்பை இயக்கு பயனர் பெயர் @@ -508,12 +508,10 @@ `$GIT_COMMON_DIR/பணிமரங்கள்` இதில் பணிமரம் தகவலை கத்தரி இழு தொலை கிளை: - எல்லா கிளைகளையும் எடு இதனுள்: உள்ளக மாற்றங்கள்: நிராகரி பதுக்கிவை & மீண்டும் இடு - குறிச்சொற்கள் இல்லாமல் பெறு தொலை: இழு (எடுத்து ஒன்றிணை) ஒன்றிணை என்பதற்குப் பதிலாக மறுதளத்தைப் பயன்படுத்து @@ -535,7 +533,6 @@ உள்ளக மாற்றங்களை பதுக்கிவை & மீண்டும் இடு மேல்: மறுதளம்: - புதுப்பி தொலையைச் சேர் தொலையைத் திருத்து பெயர்: @@ -564,6 +561,7 @@ தொடர்க தனிப்பயன் செயல்கள் தனிப்பயன் செயல்கள் இல்லை + எல்லா மாற்றங்களையும் நிராகரி '--குறிபதிவு' விருப்பத்தை இயக்கு கோப்பு உலாவியில் திற கிளைகள்/குறிச்சொற்கள்/துணைத் தொகுதிகளைத் தேடு @@ -604,8 +602,7 @@ குறிசொற்கள் புதிய குறிசொல் படைப்பாளர் தேதியின்படி - பெயர் (ஏறுவரிசை) மூலம் - பெயர் (இறகுவரிசை) மூலம் + பெயர் மூலம் வரிசைப்படுத்து முனையத்தில் திற வரலாறுகளில் உறவு நேரத்தைப் பயன்படுத்து @@ -622,7 +619,6 @@ உறுதிமொழி: பின்வாங்கு மாற்றங்களை உறுதிமொழி மாறுசொல் உறுதிமொழி செய்தி - புதிய வரியை உள்ளிட 'உயர்த்து+நுழை' ஐப் பயன்படுத்தவும். 'நுழை' என்பது சரி பொத்தானின் சூடானவிசை ஆகும் இயங்குகிறது. காத்திருக்கவும்... சேமி எனச் சேமி... @@ -648,10 +644,7 @@ தனியார் பாஓடு திறவுகோல் கடை பாதை தொடங்கு பதுக்கிவை - பதுக்கிவைத்த பிறகு தானியங்கி மீட்டமை - உங்கள் செயல்படும் கோப்புகள் மாறாமல் இருக்கும், ஆனால் ஒரு பதுக்கிவைக்கப்படும். கண்காணிக்கப்படாத கோப்புகளைச் சேர் - நிலைப்படுத்தப்பட்ட கோப்புகளை வைத்திரு செய்தி: விருப்பத்தேர்வு. இந்த பதுக்கலின் பெயர் நிலைப்படுத்தப்பட்ட மாற்றங்கள் மட்டும் @@ -712,7 +705,6 @@ அறிவிலி புறக்கணி எல்லா *{0} கோப்புகளையும் புறக்கணி ஒரே கோப்புறையில் *{0} கோப்புகளைப் புறக்கணி - ஒரே கோப்புறையில் கோப்புகளைப் புறக்கணி இந்த கோப்பை மட்டும் புறக்கணி பின்னொட்டு இந்த கோப்பை இப்போது நீங்கள் நிலைப்படுத்தலாம். @@ -723,7 +715,6 @@ உறுதிமொழி (திருத்து) அனைத்து மாற்றங்களையும் நிலைப்படுத்தி உறுதிமொழி நீங்கள் {0} கோப்புகளை நிலைப்படுத்தியுள்ளீர்கள், ஆனால் {1} கோப்புகள் மட்டுமே காட்டப்பட்டுள்ளன ({2} கோப்புகள் வடிகட்டப்பட்டுள்ளன). தொடர விரும்புகிறீர்களா? - காலி உறுதிமொழி கண்டறியப்பட்டது! தொடர விரும்புகிறீர்களா(--allow-empty)? மோதல்கள் கண்டறியப்பட்டது கோப்பு மோதல்கள் தீர்க்கப்பட்டது கண்காணிக்கப்படாத கோப்புகளைச் சேர் @@ -737,7 +728,7 @@ நிலைநீக்கு நிலைபடுத்து அனைத்தும் நிலைபடுத்து - மாறாதது எனநினைப்பதை பார் + மாறாதது எனநினைப்பதை பார் வளர்புரு: ${0}$ பணியிடம்: பணியிடங்களை உள்ளமை... diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml new file mode 100644 index 00000000..5d120117 --- /dev/null +++ b/src/Resources/Locales/uk_UA.axaml @@ -0,0 +1,750 @@ + + + + + + Про програму + Про SourceGit + Безкоштовний Git GUI клієнт з відкритим кодом + Додати робоче дерево + Розташування: + Шлях для цього робочого дерева. Відносний шлях підтримується. + Назва гілки: + Необов'язково. За замовчуванням — назва кінцевої папки. + Відстежувати гілку: + Відстежувати віддалену гілку + Що перемкнути: + Створити нову гілку + Наявна гілка + AI Асистент + ПЕРЕГЕНЕРУВАТИ + Використати AI для генерації повідомлення коміту + ЗАСТОСУВАТИ ЯК ПОВІДОМЛЕННЯ КОМІТУ + Застосувати + Файл патчу: + Виберіть файл .patch для застосування + Ігнорувати зміни пробілів + Застосувати Патч + Пробіли: + Застосувати схованку + Видалити після застосування + Відновити зміни індексу + Схованка: + Архівувати... + Зберегти архів у: + Виберіть шлях до файлу архіву + Ревізія: + Архівувати + SourceGit Askpass + ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + НЕМАЄ ФАЙЛІВ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + ВИДАЛИТИ + Оновити + БІНАРНИЙ ФАЙЛ НЕ ПІДТРИМУЄТЬСЯ!!! + Автор рядка + ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! + Перейти на ${0}$... + Порівняти з ${0}$ + Порівняти з робочим деревом + Копіювати назву гілки + Спеціальна дія + Видалити ${0}$... + Видалити вибрані {0} гілок + Перемотати до ${0}$ + Отримати ${0}$ в ${1}$... + Git Flow - Завершити ${0}$ + Злиття ${0}$ в ${1}$... + Злити вибрані {0} гілок в поточну + Витягти ${0}$ + Витягти ${0}$ в ${1}$... + Надіслати ${0}$ + Перебазувати ${0}$ на ${1}$... + Перейменувати ${0}$... + Встановити відстежувану гілку... + Порівняти гілки + Недійсний upstream! + Байтів + СКАСУВАТИ + Скинути до батьківської ревізії + Скинути до цієї ревізії + Згенерувати повідомлення коміту + ЗМІНИТИ РЕЖИМ ВІДОБРАЖЕННЯ + Показати як список файлів та тек + Показати як список шляхів + Показати як дерево файлової системи + Перейти на гілку + Перейти на коміт + Коміт: + Попередження: Перехід на коміт призведе до стану "від'єднаний HEAD" + Локальні зміни: + Скасувати + Сховати та Застосувати + Гілка: + Cherry-pick + Додати джерело до повідомлення коміту + Коміт(и): + Закомітити всі зміни + Батьківський коміт: + Зазвичай неможливо cherry-pick злиття, бо невідомо, яку сторону злиття вважати батьківською (mainline). Ця опція дозволяє відтворити зміни відносно вказаного батьківського коміту. + Очистити схованки + Ви намагаєтеся очистити всі схованки. Ви впевнені? + Клонувати віддалене сховище + Додаткові параметри: + Додаткові аргументи для клонування сховища. Необов'язково. + Локальна назва: + Назва сховища. Необов'язково. + Батьківська тека: + Ініціалізувати та оновити підмодулі + URL сховища: + ЗАКРИТИ + Редактор + Перейти на коміт + Cherry-pick коміт + Cherry-pick ... + Порівняти з HEAD + Порівняти з робочим деревом + Iнформацію + SHA + Спеціальна дія + Інтерактивно перебазувати ${0}$ сюди + Злиття в ${0}$ + Злити ... + Перебазувати ${0}$ сюди + Скинути ${0}$ сюди + Скасувати коміт + Змінити повідомлення + Зберегти як патч... + Склеїти з батьківським комітом + Склеїти дочірні коміти сюди + ЗМІНИ + Пошук змін... + ФАЙЛИ + LFS Файл + Пошук файлів... + Підмодуль + ІНФОРМАЦІЯ + АВТОР + ЗМІНЕНО + ДОЧІРНІ + КОМІТЕР + Перевірити посилання, що містять цей коміт + КОМІТ МІСТИТЬСЯ В + Показано лише перші 100 змін. Дивіться всі зміни на вкладці ЗМІНИ. + ПОВІДОМЛЕННЯ + БАТЬКІВСЬКІ + ПОСИЛАННЯ (Refs) + SHA + Відкрити в браузері + Опис + Введіть тему коміту + Налаштування сховища + ШАБЛОН КОМІТУ + Зміст шаблону: + Назва шаблону: + СПЕЦІАЛЬНА ДІЯ + Аргументи: + ${REPO} - Шлях до сховища; ${BRANCH} - Вибрана гілка; ${SHA} - SHA вибраного коміту + Виконуваний файл: + Назва: + Область застосування: + Гілка + Коміт + Репозиторій + Чекати завершення дії + Адреса Email + Адреса електронної пошти + GIT + Автоматично отримувати зміни з віддалених сховищ + хвилин(и) + Віддалене сховище за замовчуванням + Бажаний режим злиття + ТРЕКЕР ЗАВДАНЬ + Додати приклад правила для Azure DevOps + Додати приклад правила для Gitee Issue + Додати приклад правила для Gitee Pull Request + Додати приклад правила для Github + Додати приклад правила для GitLab Issue + Додати приклад правила для GitLab Merge Request + Додати приклад правила для Jira + Нове правило + Регулярний вираз для завдання: + Назва правила: + URL результату: + Використовуйте $1, $2 для доступу до значень груп регулярного виразу. + AI + Бажаний сервіс: + Якщо 'Бажаний сервіс' встановлено, SourceGit буде використовувати лише його у цьому сховищі. Інакше, якщо доступно більше одного сервісу, буде показано контекстне меню для вибору. + HTTP Проксі + HTTP проксі, що використовується цим сховищем + Ім'я користувача + Ім'я користувача для цього сховища + Робочі простори + Колір + Відновлювати вкладки при запуску + ПРОДОВЖИТИ + Виявлено порожній коміт! Продовжити (--allow-empty)? + ІНДЕКСУВАТИ ВСЕ ТА ЗАКОМІТИТИ + Виявлено порожній коміт! Продовжити (--allow-empty) чи індексувати все та закомітити? + Допомога Conventional Commit + Зворотньо несумісні зміни: + Закрите завдання: + Детальні зміни: + Область застосування: + Короткий опис: + Тип зміни: + Копіювати + Копіювати весь текст + Копіювати повний шлях + Копіювати шлях + Створити гілку... + На основі: + Перейти на створену гілку + Локальні зміни: + Скасувати + Сховати та Застосувати + Назва нової гілки: + Введіть назву гілки. + Пробіли будуть замінені на тире. + Створити локальну гілку + Створити тег... + Новий тег для: + Підпис GPG + Повідомлення тегу: + Необов'язково. + Назва тегу: + Рекомендований формат: v1.0.0-alpha + Надіслати на всі віддалені сховища після створення + Створити Новий Тег + Тип: + анотований + легкий + Утримуйте Ctrl для запуску без діалогу + Вирізати + Видалити гілку + Гілка: + Ви збираєтеся видалити віддалену гілку!!! + Також видалити віддалену гілку ${0}$ + Видалити кілька гілок + Ви намагаєтеся видалити кілька гілок одночасно. Перевірте ще раз перед виконанням! + Видалити віддалене сховище + Віддалене сховище: + Шлях: + Ціль: + Усі дочірні елементи будуть видалені зі списку. + Це видалить сховище лише зі списку, а не з диска! + Підтвердити видалення групи + Підтвердити видалення сховища + Видалити підмодуль + Шлях до підмодуля: + Видалити тег + Тег: + Видалити з віддалених сховищ + РІЗНИЦЯ ДЛЯ БІНАРНИХ ФАЙЛІВ + НОВИЙ + СТАРИЙ + Копіювати + Змінено режим файлу + Перша відмінність + Ігнорувати зміни пробілів + Остання відмінність + ЗМІНА ОБ'ЄКТА LFS + Наступна відмінність + НЕМАЄ ЗМІН АБО ЛИШЕ ЗМІНИ КІНЦЯ РЯДКА + Попередня відмінність + Зберегти як патч + Показати приховані символи + Порівняння пліч-о-пліч + ПІДМОДУЛЬ + НОВИЙ + Поміняти місцями + Підсвітка синтаксису + Перенос слів + Увімкнути навігацію блоками + Відкрити в інструменті злиття + Показати всі рядки + Зменшити кількість видимих рядків + Збільшити кількість видимих рядків + ОБЕРІТЬ ФАЙЛ ДЛЯ ПЕРЕГЛЯДУ ЗМІН + Відкрити в інструменті злиття + Скасувати зміни + Усі локальні зміни в робочій копії. + Зміни: + Включити файли, які ігноруються + {0} змін будуть відхилені + Ви не можете скасувати цю дію!!! + Закладка: + Нова назва: + Ціль: + Редагувати вибрану групу + Редагувати вибраний репозиторій + Виконати спеціальну дію + Ім'я дії: + Витягти + Витягти всі віддалені сховища + Примусово перезаписати локальні refs + Витягти без тегів + Віддалений: + Витягти зміни з віддалених репозиторіїв + Вважати незмінними + Скасувати... + Скасувати {0} файлів... + Скасувати зміни в вибраних рядках + Відкрити зовнішній інструмент злиття + Розв'язати за допомогою ${0}$ + Зберегти як патч... + Стагнути + Стагнути {0} файлів + Стагнути зміни в вибраних рядках + Схованка... + Схованка {0} файлів... + Скинути стаг + Скинути {0} файлів + Скинути зміни в вибраних рядках + Використовувати Mine (checkout --ours) + Використовувати Theirs (checkout --theirs) + Історія файлу + ЗМІНА + ЗМІСТ + Git-Flow + Розробка гілки: + Функція: + Префікс функції: + FLOW - Завершити функцію + FLOW - Завершити гарячу поправку + FLOW - Завершити реліз + Ціль: + Гаряча поправка: + Префікс гарячої поправки: + Ініціалізувати Git-Flow + Залишити гілку + Гілка виробництва: + Реліз: + Префікс релізу: + Почати функцію... + FLOW - Почати функцію + Почати гарячу поправку... + FLOW - Почати гарячу поправку + Введіть назву + Почати реліз... + FLOW - Почати реліз + Тег версії Префікс: + Git LFS + Додати шаблон для відстеження... + Шаблон є ім'ям файлу + Спеціальний шаблон: + Додати шаблон для відстеження до Git LFS + Витягти + Запустіть `git lfs fetch`, щоб завантажити об'єкти Git LFS. Це не оновлює робочу копію. + Витягти об'єкти LFS + Встановити Git LFS hooks + Показати блокування + Немає заблокованих файлів + Заблокувати + Показати лише мої блокування + LFS блокування + Розблокувати + Примусово розблокувати + Принт + Запустіть `git lfs prune`, щоб видалити старі файли з локального сховища + Витягти + Запустіть `git lfs pull`, щоб завантажити всі файли Git LFS для поточної ref & checkout + Витягти об'єкти LFS + Надіслати + Надіслати чернетки великих файлів до кінця Git LFS + Надіслати об'єкти LFS + Віддалений: + Відстежувати файли, названі '{0}' + Відстежувати всі *{0} файли + ІСТОРІЯ + АВТОР + ЧАС АВТОРА + ГРАФ ТА ТЕМА + SHA + ЧАС КОМІТУ + ВИБРАНО {0} КОМІТІВ + Утримуйте 'Ctrl' або 'Shift' для вибору кількох комітів. + Утримуйте ⌘ або ⇧ для вибору кількох комітів. + ПОРАДИ: + Гарячі клавіші + ГЛОБАЛЬНІ + Скасувати поточне спливаюче вікно + Клонувати нове сховище + Закрити поточну вкладку + Перейти до наступної вкладки + Перейти до попередньої вкладки + Створити нову вкладку + Відкрити діалог Налаштування + СХОВИЩЕ + Закомітити проіндексовані зміни + Закомітити та надіслати проіндексовані зміни + Індексувати всі зміни та закомітити + Створити нову гілку на основі вибраного коміту + Скасувати вибрані зміни + Fetch, запускається без діалогу + Режим панелі керування (за замовчуванням) + Режим пошуку комітів + Pull, запускається без діалогу + Push, запускається без діалогу + Примусово перезавантажити це сховище + Індексувати/Видалити з індексу вибрані зміни + Перейти до 'Зміни' + Перейти до 'Історія' + Перейти до 'Схованки' + ТЕКСТОВИЙ РЕДАКТОР + Закрити панель пошуку + Знайти наступний збіг + Знайти попередній збіг + Відкрити панель пошуку + Скасувати + Індексувати + Видалити з індексу + Ініціалізувати сховище + Шлях: + Cherry-pick в процесі. + Обробка коміту + Злиття в процесі. + Виконується злиття + Перебазування в процесі. + Зупинено на + Скасування в процесі. + Скасування коміту + Інтерактивне перебазування + На: + Цільова гілка: + Копіювати посилання + Відкрити в браузері + ПОМИЛКА + ПОВІДОМЛЕННЯ + Злиття гілки + В: + Опція злиття: + Джерело: + Злиття (Кілька) + Закомітити всі зміни + Стратегія: + Цілі: + Перемістити вузол сховища + Виберіть батьківський вузол для: + Назва: + Git не налаштовано. Будь ласка, перейдіть до [Налаштування] та налаштуйте його. + Відкрити теку зберігання даних + Відкрити за допомогою... + Необов'язково. + Створити нову вкладку + Закладка + Закрити вкладку + Закрити інші вкладки + Закрити вкладки праворуч + Копіювати шлях до сховища + Сховища + Вставити + {0} днів тому + годину тому + {0} годин тому + Щойно + Минулого місяця + Минулого року + {0} хвилин тому + {0} місяців тому + {0} років тому + Вчора + Використовуйте 'Shift+Enter' для введення нового рядка. 'Enter' - гаряча клавіша кнопки OK + Налаштування + AI + Промпт для аналізу різниці + Ключ API + Промпт для генерації теми + Модель + Назва + Сервер + Увімкнути потокове відтворення + ВИГЛЯД + Шрифт за замовчуванням + Ширина табуляції в редакторі + Розмір шрифту + За замовчуванням + Редактор + Моноширинний шрифт + Використовувати моноширинний шрифт лише в текстовому редакторі + Тема + Перевизначення теми + Використовувати фіксовану ширину вкладки в заголовку + Використовувати системну рамку вікна + ІНСТРУМЕНТ DIFF/MERGE + Шлях встановлення + Введіть шлях до інструменту diff/merge + Інструмент + ЗАГАЛЬНІ + Перевіряти оновлення при запуску + Формат дати + Мова + Кількість комітів в історії + Показувати час автора замість часу коміту в графі + Показувати дочірні коміти в деталях + Показувати теги в графі комітів + Довжина лінії-орієнтира для теми + GIT + Увімкнути авто-CRLF + Тека клонування за замовчуванням + Email користувача + Глобальний email користувача git + Увімкнути --prune при fetch + Git (>= 2.25.1) є обов'язковим для цієї програми + Шлях встановлення + Увімкнути перевірку HTTP SSL + Ім'я користувача + Глобальне ім'я користувача git + Версія Git + ПІДПИС GPG + Підпис GPG для комітів + Формат GPG + Шлях встановлення програми + Введіть шлях до встановленої програми GPG + Підпис GPG для тегів + Ключ підпису користувача + Ключ підпису GPG користувача + ІНТЕГРАЦІЯ + КОНСОЛЬ/ТЕРМІНАЛ + Шлях + Консоль/Термінал + Prune для віддаленого сховища + Ціль: + Prune для робочих дерев + Видалити застарілу інформацію про робочі дерева в `$GIT_COMMON_DIR/worktrees` + Pull (Витягти) + Віддалена гілка: + В: + Локальні зміни: + Скасувати + Сховати та Застосувати + Віддалене сховище: + Pull (Fetch & Merge) + Використовувати rebase замість merge + Push (Надіслати) + Переконатися, що підмодулі надіслано + Примусовий push + Локальна гілка: + Віддалене сховище: + Надіслати зміни на віддалене сховище + Віддалена гілка: + Встановити як відстежувану гілку + Надіслати всі теги + Надіслати тег на віддалене сховище + Надіслати на всі віддалені сховища + Віддалене сховище: + Тег: + Вийти + Перебазувати поточну гілку + Сховати та застосувати локальні зміни + На: + Перебазувати: + Додати віддалене сховище + Редагувати віддалене сховище + Назва: + Назва віддаленого сховища + URL сховища: + URL віддаленого git сховища + Копіювати URL + Видалити... + Редагувати... + Fetch (Отримати) + Відкрити у браузері + Prune (Очистити) + Підтвердити видалення робочого дерева + Увімкнути опцію `--force` + Ціль: + Перейменувати гілку + Нова назва: + Унікальна назва для цієї гілки + Гілка: + ПЕРЕРВАТИ + Автоматичне отримання змін з віддалених сховищ... + Очистка (GC & Prune) + Виконати команду `git gc` для цього сховища. + Очистити все + Налаштувати це сховище + ПРОДОВЖИТИ + Спеціальні дії + Немає спеціальних дій + Скасувати всі зміни + Увімкнути опцію '--reflog' + Відкрити у файловому менеджері + Пошук гілок/тегів/підмодулів + Видимість у графі + Не встановлено + Приховати в графі комітів + Фільтрувати в графі комітів + Увімкнути опцію '--first-parent' + РОЗТАШУВАННЯ + Горизонтальне + Вертикальне + ПОРЯДОК КОМІТІВ + За датою коміту + Топологічний + ЛОКАЛЬНІ ГІЛКИ + Перейти до HEAD + Створити гілку + ОЧИСТИТИ СПОВІЩЕННЯ + Виділяти лише поточну гілку в графі + Відкрити в {0} + Відкрити в зовнішніх інструментах + Оновити + ВІДДАЛЕНІ СХОВИЩА + ДОДАТИ ВІДДАЛЕНЕ СХОВИЩЕ + Пошук коміту + Автор + Комітер + Файл + Повідомлення + SHA + Поточна гілка + Показати теги як дерево + ПРОПУСТИТИ + Статистика + ПІДМОДУЛІ + ДОДАТИ ПІДМОДУЛЬ + ОНОВИТИ ПІДМОДУЛЬ + ТЕГИ + НОВИЙ ТЕГ + За датою створення + За назвою + Сортувати + Відкрити в терміналі + Використовувати відносний час в історії + РОБОЧІ ДЕРЕВА + ДОДАТИ РОБОЧЕ ДЕРЕВО + PRUNE (ОЧИСТИТИ) + URL Git сховища + Скинути поточну гілку до ревізії + Режим скидання: + Перемістити до: + Поточна гілка: + Показати у файловому менеджері + Revert (Скасувати коміт) + Коміт: + Закомітити зміни скасування + Змінити повідомлення коміту + Виконується. Будь ласка, зачекайте... + ЗБЕРЕГТИ + Зберегти як... + Патч успішно збережено! + Сканувати сховища + Коренева тека: + Перевірити оновлення... + Доступна нова версія програми: + Не вдалося перевірити оновлення! + Завантажити + Пропустити цю версію + Оновлення програми + У вас встановлена остання версія. + Встановити відстежувану гілку + Гілка: + Скасувати upstream + Upstream: + Копіювати SHA + Перейти до + Squash (Склеїти коміти) + В: + Приватний ключ SSH: + Шлях до сховища приватного ключа SSH + ПОЧАТИ + Stash (Сховати) + Включити невідстежувані файли + Повідомлення: + Необов'язково. Назва цієї схованки + Лише проіндексовані зміни + Будуть сховані як проіндексовані, так і не проіндексовані зміни вибраних файлів!!! + Сховати локальні зміни + Застосувати + Видалити + Зберегти як патч... + Видалити схованку + Видалити: + СХОВАНКИ + ЗМІНИ + СХОВАНКИ + Статистика + КОМІТИ + КОМІТЕР + ОГЛЯД + МІСЯЦЬ + ТИЖДЕНЬ + АВТОРІВ: + КОМІТІВ: + ПІДМОДУЛІ + Додати підмодуль + Копіювати відносний шлях + Отримати вкладені підмодулі + Відкрити сховище підмодуля + Відносний шлях: + Відносна тека для зберігання цього модуля. + Видалити підмодуль + OK + Копіювати назву тегу + Копіювати повідомлення тегу + Видалити ${0}$... + Злиття ${0}$ в ${1}$... + Надіслати ${0}$... + Оновити підмодулі + Усі підмодулі + Ініціалізувати за потреби + Рекурсивно + Підмодуль: + Використовувати опцію --remote + URL: + Попередження + Вітальна сторінка + Створити групу + Створити підгрупу + Клонувати сховище + Видалити + ПІДТРИМУЄТЬСЯ ПЕРЕТЯГУВАННЯ ТЕК. МОЖЛИВЕ ГРУПУВАННЯ. + Редагувати + Перемістити до іншої групи + Відкрити всі сховища + Відкрити сховище + Відкрити термінал + Пересканувати сховища у теці клонування за замовчуванням + Пошук сховищ... + Сортувати + ЛОКАЛЬНІ ЗМІНИ + Git Ignore + Ігнорувати всі файли *{0} + Ігнорувати файли *{0} у цій же теці + Ігнорувати лише цей файл + Amend (Доповнити) + Тепер ви можете проіндексувати цей файл. + КОМІТ + КОМІТ ТА PUSH + Шаблон/Історії + Викликати подію кліку + Коміт (Редагувати) + Індексувати всі зміни та закомітити + Ви проіндексували {0} файл(ів), але відображено лише {1} ({2} файлів відфільтровано). Продовжити? + ВИЯВЛЕНО КОНФЛІКТИ + ВІДКРИТИ ЗОВНІШНІЙ ІНСТРУМЕНТ ЗЛИТТЯ + ВІДКРИТИ ВСІ КОНФЛІКТИ В ЗОВНІШНЬОМУ ІНСТРУМЕНТІ ЗЛИТТЯ + КОНФЛІКТИ ФАЙЛІВ ВИРІШЕНО + ВИКОРИСТАТИ МОЮ ВЕРСІЮ + ВИКОРИСТАТИ ЇХНЮ ВЕРСІЮ + ВКЛЮЧИТИ НЕВІДСТЕЖУВАНІ ФАЙЛИ + НЕМАЄ ОСТАННІХ ПОВІДОМЛЕНЬ + НЕМАЄ ШАБЛОНІВ КОМІТІВ + Клацніть правою кнопкою миші на вибраних файлах та оберіть спосіб вирішення конфліктів. + Підпис + ПРОІНДЕКСОВАНІ + ВИДАЛИТИ З ІНДЕКСУ + ВИДАЛИТИ ВСЕ З ІНДЕКСУ + НЕПРОІНДЕКСОВАНІ + ІНДЕКСУВАТИ + ІНДЕКСУВАТИ ВСЕ + ПЕРЕГЛЯНУТИ ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + Шаблон: ${0}$ + РОБОЧИЙ ПРОСТІР: + Налаштувати робочі простори... + РОБОЧЕ ДЕРЕВО + Копіювати шлях + Заблокувати + Видалити + Розблокувати + diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 35a97b6e..2a5991fd 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -39,17 +39,25 @@ 不跟踪更改的文件 没有不跟踪更改的文件 移除 + 加载本地图片 + 重新加载 二进制文件不支持该操作!!! + 二分定位(bisect) + 终止 + 标记错误 + 二分定位进行中。当前提交是 '正确' 还是 '错误' ? + 标记正确 + 无法判定 + 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout) ${0}$... - 与当前HEAD比较 + 与当前 ${0}$ 比较 与本地工作树比较 复制分支名 自定义操作 删除 ${0}$... 删除选中的 {0} 个分支 - 放弃所有更改 快进(fast-forward)到 ${0}$ 拉取(fetch) ${0}$ 至 ${1}$... GIT工作流 - 完成 ${0}$ @@ -58,8 +66,9 @@ 拉回(pull) ${0}$ 拉回(pull) ${0}$ 内容至 ${1}$... 推送(push)${0}$ - 变基(rebase) ${0}$ 分支至 ${1}$... + 变基(rebase) ${0}$ 至 ${1}$... 重命名 ${0}$... + 重置 ${0}$ 到 ${1}$... 切换上游分支 ... 分支比较 跟踪的上游分支不存在或已删除! @@ -79,7 +88,10 @@ 未提交更改 : 丢弃更改 贮藏并自动恢复 + 同时更新所有子模块 目标分支 : + 检出分支并快进 + 上游分支 : 挑选提交 提交信息中追加来源信息 提交列表 : @@ -103,12 +115,16 @@ 挑选(cherry-pick)... 与当前HEAD比较 与本地工作树比较 - 复制简要信息 - 复制提交指纹 + 作者 + 提交者 + 简要信息 + 提交指纹 + 主题 自定义操作 交互式变基(rebase -i) ${0}$ 到此处 合并(merge)此提交至 ${0}$ 合并(merge)... + 推送(push) ${0}$ 到 ${1}$ 变基(rebase) ${0}$ 到此处 重置(reset) ${0}$ 到此处 回滚此提交 @@ -117,6 +133,7 @@ 合并此提交到上一个提交 合并之后的提交到此处 变更对比 + 个文件发生变更 查找变更... 文件列表 LFS文件 @@ -136,6 +153,7 @@ 提交指纹 浏览器中查看 详细描述 + 主题 填写提交信息主题 仓库配置 提交信息模板 @@ -180,6 +198,7 @@ 应用于本仓库的用户名 工作区 颜色 + 名称 启动时恢复打开的仓库 确认继续 提交未包含变更文件!是否继续(--allow-empty)? @@ -206,6 +225,7 @@ 填写分支名称。 空格将被替换为'-'符号 创建本地分支 + 允许重置已存在的分支 新建标签 ... 标签位于 : 使用GPG签名 @@ -220,6 +240,9 @@ 轻量标签 按住Ctrl键点击将以默认参数运行 剪切 + 取消初始化子模块 + 强制取消,即使包含本地变更 + 子模块 : 删除分支确认 分支名 : 您正在删除远程上的分支,请务必小心!!! @@ -255,6 +278,7 @@ 显示隐藏符号 分列对比 子模块 + 删除 新增 交换比对双方 语法高亮 @@ -279,7 +303,6 @@ 编辑仓库 执行自定义操作 自定义操作 : - 快进(fast-forward,无需checkout) 拉取(fetch) 拉取所有的远程仓库 强制覆盖本地REFs @@ -314,6 +337,8 @@ 结束修复分支 结束版本分支 目标分支 : + 完成后自动推送 + 压缩变更为单一提交后合并分支 修复分支 : 修复分支名前缀 : 初始化GIT工作流 @@ -375,6 +400,8 @@ 切换到上一个页面 新建页面 打开偏好设置面板 + 切换工作区 + 切换显示页面 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 @@ -395,6 +422,7 @@ 关闭搜索 定位到下一个匹配搜索的位置 定位到上一个匹配搜索的位置 + 使用外部比对工具查看 打开搜索 丢弃 暂存 @@ -416,7 +444,10 @@ 在浏览器中访问 出错了 系统提示 + 工作区列表 + 页面列表 合并分支 + 编辑合并信息 目标分支 : 合并方式 : 合并目标 : @@ -449,6 +480,7 @@ {0}个月前 {0}年前 昨天 + 请使用Shift+Enter换行。Enter键已被【确 定】按钮占用。 偏好设置 AI Analyze Diff Prompt @@ -460,7 +492,6 @@ 启用流式输出 外观配置 缺省字体 - 代码字体大小 编辑器制表符宽度 字体大小 默认 @@ -490,7 +521,8 @@ 邮箱 默认GIT用户邮箱 拉取更新时启用修剪(--prune) - 本软件要求GIT最低版本为2.23.0 + 对比文件时,默认忽略换行符变更 (--ignore-cr-at-eol) + 本软件要求GIT最低版本为2.25.1 安装路径 启用HTTP SSL验证 用户名 @@ -514,12 +546,11 @@ 清理在`$GIT_COMMON_DIR/worktrees`中的无效工作树信息 拉回(pull) 拉取分支 : - 拉取远程中的所有分支变更 本地分支 : 未提交更改 : 丢弃更改 贮藏并自动恢复 - 不拉取远程标签 + 同时更新所有子模块 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -528,6 +559,8 @@ 启用强制推送 本地分支 : 远程仓库 : + 修订 : + 推送指定修订到远程仓库 推送到远程仓库 远程分支 : 跟踪远程分支 @@ -541,7 +574,6 @@ 自动贮藏并恢复本地变更 目标提交 : 分支 : - 重新加载 添加远程仓库 编辑远程仓库 远程名 : @@ -563,13 +595,18 @@ 分支 : 终止合并 自动拉取远端变更中... + 排序方式 + 按提交时间 + 按名称 清理本仓库(GC) 本操作将执行`git gc`命令。 清空过滤规则 + 清空 配置本仓库 下一步 自定义操作 自定义操作未设置 + 放弃所有更改 启用 --reflog 选项 在文件浏览器中打开 快速查找分支/标签/子模块 @@ -597,10 +634,12 @@ 查找提交 作者 提交者 + 变更内容 文件 提交信息 提交指纹 仅在当前分支中查找 + 以树型结构展示 以树型结构展示 跳过此提交 提交统计 @@ -610,11 +649,12 @@ 标签列表 新建标签 按创建时间 - 按名称(升序) - 按名称(降序) + 按名称 排序 在终端中打开 在提交列表中使用相对时间 + 查看命令日志 + 访问远程仓库 '{0}' 工作树列表 新增工作树 清理 @@ -623,12 +663,14 @@ 重置模式 : 提交 : 当前分支 : + 重置所选分支(非当前分支) + 重置点 : + 操作分支 : 在文件浏览器中查看 回滚操作确认 目标提交 : 回滚后提交更改 编辑提交信息 - 请使用Shift+Enter换行。Enter键已被【确 定】按钮占用。 执行操作中,请耐心等待... 保 存 另存为... @@ -654,16 +696,15 @@ SSH密钥文件 开 始 贮藏(stash) - 贮藏后自动恢复工作区 - 工作区文件保持未修改状态,但贮藏内容已保存。 包含未跟踪的文件 - 保留暂存区文件 信息 : - 选填,用于命名此贮藏 + 选填,此贮藏的描述信息 + 模式 : 仅贮藏暂存区的变更 选中文件的所有变更均会被贮藏! 贮藏本地变更 应用(apply) + 复制描述信息 删除(drop) 另存为补丁... 丢弃贮藏确认 @@ -682,11 +723,18 @@ 子模块 添加子模块 复制路径 + 取消初始化 拉取子孙模块 打开仓库 相对仓库路径 : 本地存放的相对路径。 删除子模块 + 状态 + 未提交修改 + 未初始化 + SHA变更 + 未解决冲突 + 仓库 确 定 复制标签名 复制标签信息 @@ -700,6 +748,10 @@ 子模块 : 启用 '--remote' 仓库地址 : + 日志列表 + 清空日志 + 复制 + 删除 警告 起始页 新建分组 @@ -719,7 +771,7 @@ 添加至 .gitignore 忽略列表 忽略所有 *{0} 文件 忽略同目录下所有 *{0} 文件 - 忽略同目录下所有文件 + 忽略该目录下的新文件 忽略本文件 修补 现在您已可将其加入暂存区中 @@ -739,6 +791,7 @@ 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 + 重置提交者 请选中冲突文件,打开右键菜单,选择合适的解决方式 署名 已暂存 @@ -747,7 +800,7 @@ 未暂存 暂存选中 暂存所有 - 查看忽略变更文件 + 查看忽略变更文件 模板:${0}$ 工作区: 配置工作区... diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index d71281f7..f1736ff8 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -39,17 +39,25 @@ 不追蹤變更的檔案 沒有不追蹤變更的檔案 移除 + 載入本機圖片... + 重新載入 二進位檔案不支援該操作! + 二分搜尋 (bisect) + 中止 + 標記為錯誤 + 二分搜尋進行中。目前的提交是「良好」是「錯誤」? + 標記為良好 + 無法確認 + 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 逐行溯源 (blame) 所選擇的檔案不支援該操作! 簽出 (checkout) ${0}$... - 與目前 HEAD 比較 + 與目前 ${0}$ 比較 與本機工作區比較 複製分支名稱 自訂動作 刪除 ${0}$... 刪除所選的 {0} 個分支 - 捨棄所有變更 快轉 (fast-forward) 到 ${0}$ 提取 (fetch) ${0}$ 到 ${1}$... Git 工作流 - 完成 ${0}$ @@ -60,9 +68,10 @@ 推送 (push) ${0}$ 重定基底 (rebase) ${0}$ 分支至 ${1}$... 重新命名 ${0}$... + 重設 ${0}$ 至 ${1}$... 切換上游分支... 分支比較 - 追蹤上游分支不存在或已刪除! + 追蹤上游分支不存在或已刪除! 位元組 取 消 重設檔案到上一版本 @@ -79,7 +88,10 @@ 未提交變更: 捨棄變更 擱置變更並自動復原 + 同時更新所有子模組 目標分支: + 簽出分支並快轉 + 上游分支 : 揀選提交 提交資訊中追加來源資訊 提交列表: @@ -103,12 +115,16 @@ 揀選 (cherry-pick)... 與目前 HEAD 比較 與本機工作區比較 - 複製摘要資訊 - 複製提交編號 + 作者 + 提交者 + 摘要資訊 + 提交編號 + 標題 自訂動作 互動式重定基底 (rebase -i) ${0}$ 到此處 合併 (merge) 此提交到 ${0}$ 合併 (merge)... + 推送(push) ${0}$ 至 ${1}$ 重定基底 (rebase) ${0}$ 到此處 重設 (reset) ${0}$ 到此處 復原此提交 @@ -117,6 +133,7 @@ 合併此提交到上一個提交 合併之後的提交到此處 變更對比 + 個檔案已變更 搜尋變更... 檔案列表 LFS 檔案 @@ -136,6 +153,7 @@ 提交編號 在瀏覽器中檢視 詳細描述 + 標題 填寫提交訊息標題 存放庫設定 提交訊息範本 @@ -157,7 +175,7 @@ 啟用定時自動提取 (fetch) 遠端更新 分鐘 預設遠端存放庫 - 首選合併模式 + 預設合併模式 Issue 追蹤 新增符合 Azure DevOps 規則 新增符合 Gitee 議題規則 @@ -180,11 +198,12 @@ 用於本存放庫的使用者名稱 工作區 顏色 + 名稱 啟動時還原上次開啟的存放庫 - 确认继续 + 確認繼續 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)? - 自动暂存并提交 - 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)或者自動暫存全部變更並提交? + 自動暫存並提交 + 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交? 產生約定式提交訊息 破壞性變更: 關閉的 Issue: @@ -206,6 +225,7 @@ 輸入分支名稱。 空格將以英文破折號取代 建立本機分支 + 允許覆寫現有分支 新增標籤... 標籤位於: 使用 GPG 簽章 @@ -220,6 +240,9 @@ 輕量標籤 按住 Ctrl 鍵將直接以預設參數執行 剪下 + 取消初始化子模組 + 強制取消,即使它包含本地變更 + 子模組 : 刪除分支確認 分支名稱: 您正在刪除遠端上的分支,請務必小心! @@ -255,6 +278,7 @@ 顯示隱藏符號 並排對比 子模組 + 已刪除 新增 交換比對雙方 語法上色 @@ -279,7 +303,6 @@ 編輯存放庫 執行自訂動作 自訂動作: - 快進 (fast-forward,無需 checkout) 提取 (fetch) 提取所有的遠端存放庫 強制覆寫本機 REFs @@ -301,8 +324,8 @@ 取消暫存 從暫存中移除 {0} 個檔案 取消暫存選取的變更 - 使用我方版本 (checkout --ours) - 使用對方版本 (checkout --theirs) + 使用我方版本 (ours) + 使用對方版本 (theirs) 檔案歷史 檔案變更 檔案内容 @@ -314,6 +337,8 @@ 完成修復分支 完成發行分支 目標分支: + 完成後自動推送 + 壓縮為單一提交後合併 修復分支: 修復分支前置詞: 初始化 Git 工作流 @@ -375,6 +400,8 @@ 切換到上一個頁面 新增頁面 開啟偏好設定面板 + 切換工作區 + 切換目前頁面 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 @@ -395,6 +422,7 @@ 關閉搜尋面板 前往下一個搜尋相符的位置 前往上一個搜尋相符的位置 + 使用外部比對工具檢視 開啟搜尋面板 捨棄 暫存 @@ -416,7 +444,10 @@ 在瀏覽器中開啟連結 發生錯誤 系統提示 + 工作區列表 + 頁面列表 合併分支 + 編輯合併訊息 目標分支: 合併方式: 合併來源: @@ -449,6 +480,7 @@ {0} 個月前 {0} 年前 昨天 + 請使用 Shift + Enter 換行。Enter 鍵已被 [確定] 按鈕佔用。 偏好設定 AI 分析變更差異提示詞 @@ -460,7 +492,7 @@ 啟用串流輸出 外觀設定 預設字型 - 編輯器制表符寬度 + 編輯器 Tab 寬度 字型大小 預設 程式碼 @@ -489,7 +521,8 @@ 電子郵件 預設 Git 使用者電子郵件 拉取變更時進行清理 (--prune) - 本軟體要求 Git 最低版本為 2.23.0 + 對比檔案時,預設忽略行尾的 CR 變更 (--ignore-cr-at-eol) + 本軟體要求 Git 最低版本為 2.25.1 安裝路徑 啟用 HTTP SSL 驗證 使用者名稱 @@ -513,12 +546,11 @@ 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作區資訊 拉取 (pull) 拉取分支: - 拉取遠端中的所有分支 本機分支: 未提交變更: 捨棄變更 擱置變更並自動復原 - 不拉取遠端標籤 + 同時更新所有子模組 遠端: 拉取 (提取並合併) 使用重定基底 (rebase) 合併分支 @@ -527,6 +559,8 @@ 啟用強制推送 本機分支: 遠端存放庫: + 修訂: + 推送修訂到遠端存放庫 推送到遠端存放庫 遠端分支: 追蹤遠端分支 @@ -540,7 +574,6 @@ 自動擱置變更並復原本機變更 目標提交: 分支: - 重新載入 新增遠端存放庫 編輯遠端存放庫 遠端名稱: @@ -562,13 +595,18 @@ 分支: 中止 自動提取遠端變更中... + 排序 + 依建立時間 + 依名稱升序 清理本存放庫 (GC) 本操作將執行 `git gc` 命令。 清空篩選規則 + 清空 設定本存放庫 下一步 自訂動作 沒有自訂的動作 + 捨棄所有變更 啟用 [--reflog] 選項 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 @@ -596,10 +634,12 @@ 搜尋提交 作者 提交者 + 變更內容 檔案 提交訊息 提交編號 僅搜尋目前分支 + 以樹型結構展示 以樹型結構展示 跳過此提交 提交統計 @@ -609,11 +649,12 @@ 標籤列表 新增標籤 依建立時間 - 依名稱升序 - 依名稱降序 + 依名稱 排序 在終端機中開啟 在提交列表中使用相對時間 + 檢視 Git 指令記錄 + 檢視遠端存放庫 '{0}' 工作區列表 新增工作區 清理 @@ -622,12 +663,14 @@ 重設模式: 移至提交: 目前分支: + 重設選取的分支(非目前分支) + 重設位置 : + 選取分支 : 在檔案瀏覽器中檢視 復原操作確認 目標提交: 復原後提交變更 編輯提交訊息 - 請使用 Shift + Enter 換行。Enter 鍵已被 [確定] 按鈕佔用。 執行操作中,請耐心等待... 儲存 另存新檔... @@ -653,16 +696,15 @@ SSH 金鑰檔案 開 始 擱置變更 (stash) - 擱置變更後自動復原工作區 - 工作區檔案保持未修改,但擱置內容已儲存。 包含未追蹤的檔案 - 保留已暫存的變更 擱置變更訊息: - 選填,用於命名此擱置變更 + 選填,用於描述此擱置變更 + 操作模式: 僅擱置已暫存的變更 已選取的檔案中的變更均會被擱置! 擱置本機變更 套用 (apply) + 複製描述訊息 刪除 (drop) 另存為修補檔 (patch)... 捨棄擱置變更確認 @@ -681,11 +723,18 @@ 子模組 新增子模組 複製路徑 + 取消初始化 提取子模組 開啟存放庫 相對存放庫路徑: 本機存放的相對路徑。 刪除子模組 + 狀態 + 未提交變更 + 未初始化 + SHA 變更 + 未解決的衝突 + 存放庫 確 定 複製標籤名稱 複製標籤訊息 @@ -699,6 +748,10 @@ 子模組: 啟用 [--remote] 選項 存放庫網址: + 記錄 + 清除所有記錄 + 複製 + 刪除 警告 起始頁 新增群組 @@ -718,7 +771,7 @@ 加入至 .gitignore 忽略清單 忽略所有 *{0} 檔案 忽略同路徑下所有 *{0} 檔案 - 忽略同路徑下所有檔案 + 忽略本路徑下的新增檔案 忽略本檔案 修補 現在您已可將其加入暫存區中 @@ -728,16 +781,17 @@ 觸發點擊事件 提交 (修改原始提交) 自動暫存全部變更並提交 - 您已暫存 {0} 檔案,但只顯示 {1} 檔案 ({2} 檔案被篩選器隱藏)。您要繼續嗎? - 檢測到衝突 + 您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎? + 偵測到衝突 使用外部合併工具開啟 使用外部合併工具開啟 檔案衝突已解決 - 使用 MINE - 使用 THEIRS + 使用我方版本 (ours) + 使用對方版本 (theirs) 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 + 重設作者 請選擇發生衝突的檔案,開啟右鍵選單,選擇合適的解決方式 署名 已暫存 @@ -746,7 +800,7 @@ 未暫存 暫存選取的檔案 暫存所有檔案 - 檢視不追蹤變更的檔案 + 檢視不追蹤變更的檔案 範本: ${0}$ 工作區: 設定工作區... diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 38321356..f42160a2 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -16,7 +16,7 @@ 12 - + @@ -38,9 +38,9 @@ - + - + + + + - - - + - + - + @@ -734,7 +731,7 @@ - + @@ -940,6 +947,20 @@ + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + (); + if (textDiffView == null) + return; + + var presenter = textDiffView.FindDescendantOfType(); + if (presenter == null) + return; + + if (presenter.DataContext is Models.TextDiff combined) + combined.ScrollOffset = Vector.Zero; + else if (presenter.DataContext is ViewModels.TwoSideTextDiff twoSides) + twoSides.File = string.Empty; // Just to reset `SyncScrollOffset` without affect UI refresh. + + (DataContext as ViewModels.DiffContext)?.ToggleFullTextDiff(); + e.Handled = true; + } } } diff --git a/src/Views/DropStash.axaml b/src/Views/DropStash.axaml index 32685a2c..aa5de85a 100644 --- a/src/Views/DropStash.axaml +++ b/src/Views/DropStash.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.DropStash" x:DataType="vm:DropStash"> @@ -16,14 +15,16 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0" Text="{DynamicResource Text.StashDropConfirm.Label}"/> - - + - - - + + + diff --git a/src/Views/FastForwardWithoutCheckout.axaml b/src/Views/FastForwardWithoutCheckout.axaml deleted file mode 100644 index 16b40256..00000000 --- a/src/Views/FastForwardWithoutCheckout.axaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index e33156fb..e9c956ec 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -13,12 +13,7 @@ Icon="/App.ico" Title="{DynamicResource Text.FileHistory}" MinWidth="1280" MinHeight="720"> - - - - - - + @@ -93,7 +88,9 @@ - + + + @@ -137,7 +134,7 @@ - + + + + + + + + diff --git a/src/Views/FileHistories.axaml.cs b/src/Views/FileHistories.axaml.cs index be5affc3..6a67d120 100644 --- a/src/Views/FileHistories.axaml.cs +++ b/src/Views/FileHistories.axaml.cs @@ -1,3 +1,5 @@ +using System; + using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -23,11 +25,11 @@ namespace SourceGit.Views e.Handled = true; } - private void OnResetToSelectedRevision(object sender, RoutedEventArgs e) + private async void OnResetToSelectedRevision(object sender, RoutedEventArgs e) { if (sender is Button { DataContext: ViewModels.FileHistoriesSingleRevision single }) { - single.ResetToSelectedRevision(); + await single.ResetToSelectedRevision(); NotifyDonePanel.IsVisible = true; } @@ -49,7 +51,7 @@ namespace SourceGit.Views options.DefaultExtension = ".patch"; options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; - var storageFile = await this.StorageProvider.SaveFilePickerAsync(options); + var storageFile = await StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) await compare.SaveAsPatch(storageFile.Path.LocalPath); @@ -57,5 +59,30 @@ namespace SourceGit.Views e.Handled = true; } } + + private void OnCommitSubjectDataContextChanged(object sender, EventArgs e) + { + if (sender is Border border) + ToolTip.SetTip(border, null); + } + + private void OnCommitSubjectPointerMoved(object sender, PointerEventArgs e) + { + if (sender is Border { DataContext: Models.Commit commit } border && + DataContext is ViewModels.FileHistories vm) + { + var tooltip = ToolTip.GetTip(border); + if (tooltip == null) + ToolTip.SetTip(border, vm.GetCommitFullMessage(commit)); + } + } + + private async void OnOpenFileWithDefaultEditor(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.FileHistories { ViewContent: ViewModels.FileHistoriesSingleRevision revision }) + await revision.OpenWithDefaultEditor(); + + e.Handled = true; + } } } diff --git a/src/Views/FilterModeInGraph.axaml b/src/Views/FilterModeInGraph.axaml new file mode 100644 index 00000000..520a3836 --- /dev/null +++ b/src/Views/FilterModeInGraph.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/src/Views/FilterModeInGraph.axaml.cs b/src/Views/FilterModeInGraph.axaml.cs new file mode 100644 index 00000000..c3987f91 --- /dev/null +++ b/src/Views/FilterModeInGraph.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class FilterModeInGraph : UserControl + { + public FilterModeInGraph() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/FilterModeSwitchButton.axaml.cs b/src/Views/FilterModeSwitchButton.axaml.cs index 1d1ae30c..b3b2c3da 100644 --- a/src/Views/FilterModeSwitchButton.axaml.cs +++ b/src/Views/FilterModeSwitchButton.axaml.cs @@ -163,5 +163,3 @@ namespace SourceGit.Views } } } - - diff --git a/src/Views/GitFlowFinish.axaml b/src/Views/GitFlowFinish.axaml index 7af46fd9..fa847bba 100644 --- a/src/Views/GitFlowFinish.axaml +++ b/src/Views/GitFlowFinish.axaml @@ -2,7 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.GitFlowFinish" x:DataType="vm:GitFlowFinish"> @@ -10,16 +12,16 @@ + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Feature}}"/> + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Release}}"/> - + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Hotfix}}"/> + + + + + + IsChecked="{Binding KeepBranch, Mode=TwoWay}" + ToolTip.Tip="-k"/> diff --git a/src/Views/GitFlowStart.axaml b/src/Views/GitFlowStart.axaml index 7d2b78b2..aed970de 100644 --- a/src/Views/GitFlowStart.axaml +++ b/src/Views/GitFlowStart.axaml @@ -2,8 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.GitFlowStart" x:DataType="vm:GitFlowStart"> @@ -11,15 +13,15 @@ + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Feature}}"/> + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Release}}"/> + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Hotfix}}"/> - + - + - + - - - + + + @@ -70,13 +70,12 @@ LayoutUpdated="OnCommitListLayoutUpdated" SelectionChanged="OnCommitListSelectionChanged" ContextRequested="OnCommitListContextRequested" - DoubleTapped="OnCommitListDoubleTapped" KeyDown="OnCommitListKeyDown"> @@ -72,30 +83,28 @@ - + - - diff --git a/src/Views/LauncherPageSwitcher.axaml.cs b/src/Views/LauncherPageSwitcher.axaml.cs new file mode 100644 index 00000000..9bc0bf2d --- /dev/null +++ b/src/Views/LauncherPageSwitcher.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class LauncherPageSwitcher : UserControl + { + public LauncherPageSwitcher() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Enter && DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && PagesListBox.ItemCount > 0) + { + PagesListBox.Focus(NavigationMethod.Directional); + + if (PagesListBox.SelectedIndex < 0) + PagesListBox.SelectedIndex = 0; + else if (PagesListBox.SelectedIndex < PagesListBox.ItemCount) + PagesListBox.SelectedIndex++; + + e.Handled = true; + } + } + } +} diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 73b48f58..01711afd 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -40,7 +40,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index 12bca91f..64350e42 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -1,6 +1,7 @@ using System; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -19,6 +20,20 @@ namespace SourceGit.Views set => SetValue(IsScrollerVisibleProperty, value); } + public static readonly StyledProperty SearchFilterProperty = + AvaloniaProperty.Register(nameof(SearchFilter)); + + public string SearchFilter + { + get => GetValue(SearchFilterProperty); + set => SetValue(SearchFilterProperty, value); + } + + public AvaloniaList SelectablePages + { + get; + } = []; + public LauncherTabBar() { InitializeComponent(); @@ -69,7 +84,7 @@ namespace SourceGit.Views return; var geo = new StreamGeometry(); - var angle = Math.PI / 2; + const double angle = Math.PI / 2; var y = height + 0.5; using (var ctx = geo.Open()) { @@ -83,7 +98,6 @@ namespace SourceGit.Views ctx.BeginFigure(new Point(x, y), true); y = 1; ctx.LineTo(new Point(x, y)); - x = drawRightX - 6; } else { @@ -97,9 +111,10 @@ namespace SourceGit.Views x += 6; y = 1; ctx.ArcTo(new Point(x, y), new Size(6, 6), angle, false, SweepDirection.Clockwise); - x = drawRightX - 6; } + x = drawRightX - 6; + if (drawRightX <= LauncherTabsScroller.Bounds.Right) { ctx.LineTo(new Point(x, y)); @@ -126,6 +141,14 @@ namespace SourceGit.Views context.DrawGeometry(fill, stroke, geo); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SearchFilterProperty) + UpdateSelectablePages(); + } + private void ScrollTabs(object _, PointerWheelEventArgs e) { if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) @@ -248,13 +271,95 @@ namespace SourceGit.Views e.Handled = true; } - private void OnGotoSelectedPage(object sender, LauncherTabSelectedEventArgs e) + private void OnTabsDropdownOpened(object sender, EventArgs e) { - if (DataContext is ViewModels.Launcher vm) - vm.ActivePage = e.Page; + UpdateSelectablePages(); + } - PageSelector.Flyout?.Hide(); - e.Handled = true; + private void OnTabsDropdownClosed(object sender, EventArgs e) + { + SelectablePages.Clear(); + SearchFilter = string.Empty; + } + + private void OnTabsDropdownKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + if (TabsDropdownList.SelectedItem is ViewModels.LauncherPage page && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + } + + private void OnTabsDropdownSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && TabsDropdownList.ItemCount > 0) + { + TabsDropdownList.Focus(NavigationMethod.Directional); + + if (TabsDropdownList.SelectedIndex < 0) + TabsDropdownList.SelectedIndex = 0; + else if (TabsDropdownList.SelectedIndex < TabsDropdownList.ItemCount) + TabsDropdownList.SelectedIndex++; + + e.Handled = true; + } + } + + private void OnTabsDropdownLostFocus(object sender, RoutedEventArgs e) + { + if (sender is Control { IsFocused: false, IsKeyboardFocusWithin: false }) + PageSelector.Flyout?.Hide(); + } + + private void OnClearSearchFilter(object sender, RoutedEventArgs e) + { + SearchFilter = string.Empty; + } + + private void OnTabsDropdownItemTapped(object sender, TappedEventArgs e) + { + if (sender is Control { DataContext: ViewModels.LauncherPage page } && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + + private void UpdateSelectablePages() + { + if (DataContext is not ViewModels.Launcher vm) + return; + + SelectablePages.Clear(); + + var pages = vm.Pages; + var filter = SearchFilter?.Trim() ?? ""; + if (string.IsNullOrEmpty(filter)) + { + SelectablePages.AddRange(pages); + return; + } + + foreach (var page in pages) + { + var node = page.Node; + if (node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + (node.IsRepository && node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase))) + SelectablePages.Add(page); + } } private bool _pressedTab = false; diff --git a/src/Views/LauncherTabsSelector.axaml.cs b/src/Views/LauncherTabsSelector.axaml.cs deleted file mode 100644 index 61d7a966..00000000 --- a/src/Views/LauncherTabsSelector.axaml.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Interactivity; - -namespace SourceGit.Views -{ - public class LauncherTabSelectedEventArgs : RoutedEventArgs - { - public ViewModels.LauncherPage Page { get; } - - public LauncherTabSelectedEventArgs(ViewModels.LauncherPage page) - { - RoutedEvent = LauncherTabsSelector.PageSelectedEvent; - Page = page; - } - } - - public partial class LauncherTabsSelector : UserControl - { - public static readonly StyledProperty> PagesProperty = - AvaloniaProperty.Register>(nameof(Pages)); - - public AvaloniaList Pages - { - get => GetValue(PagesProperty); - set => SetValue(PagesProperty, value); - } - - public static readonly StyledProperty SearchFilterProperty = - AvaloniaProperty.Register(nameof(SearchFilter)); - - public string SearchFilter - { - get => GetValue(SearchFilterProperty); - set => SetValue(SearchFilterProperty, value); - } - - public static readonly RoutedEvent PageSelectedEvent = - RoutedEvent.Register(nameof(PageSelected), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - public event EventHandler PageSelected - { - add { AddHandler(PageSelectedEvent, value); } - remove { RemoveHandler(PageSelectedEvent, value); } - } - - public AvaloniaList VisiblePages - { - get; - private set; - } - - public LauncherTabsSelector() - { - VisiblePages = new AvaloniaList(); - InitializeComponent(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == PagesProperty || change.Property == SearchFilterProperty) - UpdateVisiblePages(); - } - - private void OnClearSearchFilter(object sender, RoutedEventArgs e) - { - SearchFilter = string.Empty; - } - - private void OnPageSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is ListBox { SelectedItem: ViewModels.LauncherPage page }) - { - _isProcessingSelection = true; - RaiseEvent(new LauncherTabSelectedEventArgs(page)); - _isProcessingSelection = false; - } - - e.Handled = true; - } - - private void UpdateVisiblePages() - { - if (_isProcessingSelection) - return; - - VisiblePages.Clear(); - - if (Pages == null) - return; - - var filter = SearchFilter?.Trim() ?? ""; - if (string.IsNullOrEmpty(filter)) - { - foreach (var p in Pages) - VisiblePages.Add(p); - - return; - } - - foreach (var page in Pages) - { - if (!page.Node.IsRepository) - continue; - - if (page.Node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || - page.Node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) - VisiblePages.Add(page); - } - } - - private bool _isProcessingSelection = false; - } -} - diff --git a/src/Views/MenuItemExtension.cs b/src/Views/MenuItemExtension.cs index 1c23b2ea..3775b321 100644 --- a/src/Views/MenuItemExtension.cs +++ b/src/Views/MenuItemExtension.cs @@ -1,12 +1,11 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Data; namespace SourceGit.Views { public class MenuItemExtension : AvaloniaObject { public static readonly AttachedProperty CommandProperty = - AvaloniaProperty.RegisterAttached("Command", string.Empty, false, BindingMode.OneWay); + AvaloniaProperty.RegisterAttached("Command", string.Empty); } } diff --git a/src/Views/Merge.axaml b/src/Views/Merge.axaml index 33d07f02..d4ba5d70 100644 --- a/src/Views/Merge.axaml +++ b/src/Views/Merge.axaml @@ -12,7 +12,7 @@ - + + + diff --git a/src/Views/MoveRepositoryNode.axaml.cs b/src/Views/MoveRepositoryNode.axaml.cs index 450c6924..494f4f30 100644 --- a/src/Views/MoveRepositoryNode.axaml.cs +++ b/src/Views/MoveRepositoryNode.axaml.cs @@ -10,5 +10,3 @@ namespace SourceGit.Views } } } - - diff --git a/src/Views/NameHighlightedTextBlock.cs b/src/Views/NameHighlightedTextBlock.cs index 82eb0827..49f245dd 100644 --- a/src/Views/NameHighlightedTextBlock.cs +++ b/src/Views/NameHighlightedTextBlock.cs @@ -49,21 +49,16 @@ namespace SourceGit.Views AffectsMeasure(TextProperty); } - public NameHighlightedTextBlock(string nameKey, params object[] args) - { - SetCurrentValue(TextProperty, App.Text(nameKey, args)); - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center; - } - protected override Size MeasureOverride(Size availableSize) { var text = Text; if (string.IsNullOrEmpty(text)) return base.MeasureOverride(availableSize); + var trimmed = text.Replace("$", ""); var typeface = new Typeface(FontFamily); var formatted = new FormattedText( - Text, + trimmed, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, @@ -100,20 +95,15 @@ namespace SourceGit.Views normalTypeface, FontSize, Foreground); + context.DrawText(formatted, new Point(offsetX, 0)); if (isName) { var lineY = formatted.Baseline + 2; - context.DrawText(formatted, new Point(offsetX, 0)); context.DrawLine(underlinePen, new Point(offsetX, lineY), new Point(offsetX + formatted.Width, lineY)); - offsetX += formatted.WidthIncludingTrailingWhitespace; - } - else - { - context.DrawText(formatted, new Point(offsetX, 0)); - offsetX += formatted.WidthIncludingTrailingWhitespace; } + offsetX += formatted.WidthIncludingTrailingWhitespace; isName = !isName; } } diff --git a/src/Views/PopupRunningStatus.axaml b/src/Views/PopupRunningStatus.axaml index a8467415..6a0dbdb4 100644 --- a/src/Views/PopupRunningStatus.axaml +++ b/src/Views/PopupRunningStatus.axaml @@ -15,8 +15,7 @@ - + IsChecked="{Binding ShowAuthorTimeInGraph, Mode=TwoWay}"/> + IsChecked="{Binding ShowTagsInGraph, Mode=TwoWay}"/> + IsChecked="{Binding ShowChildren, Mode=TwoWay}"/> + IsChecked="{Binding Check4UpdatesOnStartup, Mode=TwoWay}"/> @@ -257,12 +257,12 @@ + IsChecked="{Binding UseFixedTabWidth, Mode=TwoWay}"/> @@ -273,7 +273,7 @@ - + + + @@ -578,19 +583,34 @@ - - - - - - - - - + + + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 73b2e995..6856fbce 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -178,7 +178,7 @@ namespace SourceGit.Views SetIfChanged(config, "user.name", DefaultUser, ""); SetIfChanged(config, "user.email", DefaultEmail, ""); SetIfChanged(config, "user.signingkey", GPGUserKey, ""); - SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null, null); + SetIfChanged(config, "core.autocrlf", CRLFMode?.Value, null); SetIfChanged(config, "fetch.prune", EnablePruneOnFetch ? "true" : "false", "false"); SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false", "false"); SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false", "false"); @@ -250,7 +250,9 @@ namespace SourceGit.Views var selected = await StorageProvider.OpenFolderPickerAsync(options); if (selected.Count == 1) { - ViewModels.Preferences.Instance.GitDefaultCloneDir = selected[0].Path.LocalPath; + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder?.Path.ToString(); + ViewModels.Preferences.Instance.GitDefaultCloneDir = folderPath; } } catch (Exception ex) @@ -349,9 +351,7 @@ namespace SourceGit.Views if (sender is CheckBox box) { ViewModels.Preferences.Instance.UseSystemWindowFrame = box.IsChecked == true; - - var dialog = new ConfirmRestart(); - App.OpenDialog(dialog); + App.ShowWindow(new ConfirmRestart(), true); } e.Handled = true; @@ -415,6 +415,30 @@ namespace SourceGit.Views e.Handled = true; } + private void OnMoveSelectedCustomActionUp(object sender, RoutedEventArgs e) + { + if (SelectedCustomAction == null) + return; + + var idx = ViewModels.Preferences.Instance.CustomActions.IndexOf(SelectedCustomAction); + if (idx > 0) + ViewModels.Preferences.Instance.CustomActions.Move(idx - 1, idx); + + e.Handled = true; + } + + private void OnMoveSelectedCustomActionDown(object sender, RoutedEventArgs e) + { + if (SelectedCustomAction == null) + return; + + var idx = ViewModels.Preferences.Instance.CustomActions.IndexOf(SelectedCustomAction); + if (idx < ViewModels.Preferences.Instance.CustomActions.Count - 1) + ViewModels.Preferences.Instance.CustomActions.Move(idx + 1, idx); + + e.Handled = true; + } + private void UpdateGitVersion() { GitVersion = Native.OS.GitVersionString; diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index 67121826..96d308b4 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -19,8 +19,7 @@ - - + - - - - + + diff --git a/src/Views/PushRevision.axaml b/src/Views/PushRevision.axaml new file mode 100644 index 00000000..71afd300 --- /dev/null +++ b/src/Views/PushRevision.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/PushRevision.axaml.cs b/src/Views/PushRevision.axaml.cs new file mode 100644 index 00000000..6a982844 --- /dev/null +++ b/src/Views/PushRevision.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class PushRevision : UserControl + { + public PushRevision() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index b16447fa..6b26f476 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -129,7 +129,7 @@ - + + + - + + @@ -196,9 +217,26 @@ - + - + + + + + + - + - - + @@ -231,24 +283,29 @@ - + - - - + + + + - - - + + + + + diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 3208fbb8..4894d1d4 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; namespace SourceGit.Views { @@ -80,5 +81,13 @@ namespace SourceGit.Views e.Handled = true; } + + private async void OnOpenFileWithDefaultEditor(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.CommitDetail { CanOpenRevisionFileWithDefaultEditor: true } vm) + await vm.OpenRevisionFileWithDefaultEditor(vm.ViewRevisionFilePath); + + e.Handled = true; + } } } diff --git a/src/Views/Reword.axaml b/src/Views/Reword.axaml index 3ea1ad98..3abbd81b 100644 --- a/src/Views/Reword.axaml +++ b/src/Views/Reword.axaml @@ -21,7 +21,7 @@ diff --git a/src/Views/SelfUpdate.axaml.cs b/src/Views/SelfUpdate.axaml.cs index d3776aca..6f4ab94c 100644 --- a/src/Views/SelfUpdate.axaml.cs +++ b/src/Views/SelfUpdate.axaml.cs @@ -25,7 +25,6 @@ namespace SourceGit.Views TextArea.TextView.Margin = new Thickness(4, 0); TextArea.TextView.Options.EnableHyperlinks = false; TextArea.TextView.Options.EnableEmailHyperlinks = false; - } protected override void OnLoaded(RoutedEventArgs e) diff --git a/src/Views/Squash.axaml b/src/Views/Squash.axaml index 30755bae..30915029 100644 --- a/src/Views/Squash.axaml +++ b/src/Views/Squash.axaml @@ -31,7 +31,7 @@ diff --git a/src/Views/StandaloneCommitMessageEditor.axaml.cs b/src/Views/StandaloneCommitMessageEditor.axaml.cs deleted file mode 100644 index 8a49ee74..00000000 --- a/src/Views/StandaloneCommitMessageEditor.axaml.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.IO; - -using Avalonia.Interactivity; - -namespace SourceGit.Views -{ - public partial class StandaloneCommitMessageEditor : ChromelessWindow - { - public StandaloneCommitMessageEditor() - { - InitializeComponent(); - } - - public void SetFile(string file) - { - _file = file; - - var content = File.ReadAllText(file).ReplaceLineEndings("\n").Trim(); - var firstLineEnd = content.IndexOf('\n'); - if (firstLineEnd == -1) - { - Editor.SubjectEditor.Text = content; - } - else - { - Editor.SubjectEditor.Text = content.Substring(0, firstLineEnd); - Editor.DescriptionEditor.Text = content.Substring(firstLineEnd + 1).Trim(); - } - } - - protected override void OnClosed(EventArgs e) - { - base.OnClosed(e); - App.Quit(_exitCode); - } - - private void SaveAndClose(object _1, RoutedEventArgs _2) - { - if (!string.IsNullOrEmpty(_file)) - { - File.WriteAllText(_file, Editor.Text); - _exitCode = 0; - } - - Close(); - } - - private string _file = string.Empty; - private int _exitCode = -1; - } -} diff --git a/src/Views/StashChanges.axaml b/src/Views/StashChanges.axaml index 51ed87ac..52af4edd 100644 --- a/src/Views/StashChanges.axaml +++ b/src/Views/StashChanges.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" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" @@ -11,45 +12,77 @@ - + + + + + + + + + + + + + + + + + + - - + - - - + Content="{DynamicResource Text.Stash.IncludeUntracked}" + IsChecked="{Binding IncludeUntracked, Mode=TwoWay}" + ToolTip.Tip="--include-untracked"> + + + + + + + FontFamilyProperty = + AvaloniaProperty.Register(nameof(FontFamily)); + + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + public static readonly StyledProperty FontSizeProperty = + AvaloniaProperty.Register(nameof(FontSize), 13); + + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground), Brushes.White); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly StyledProperty PrefixBackgroundProperty = + AvaloniaProperty.Register(nameof(PrefixBackground), Brushes.Transparent); + + public IBrush PrefixBackground + { + get => GetValue(PrefixBackgroundProperty); + set => SetValue(PrefixBackgroundProperty, value); + } + + public static readonly StyledProperty SubjectProperty = + AvaloniaProperty.Register(nameof(Subject)); + + public string Subject + { + get => GetValue(SubjectProperty); + set => SetValue(SubjectProperty, value); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var subject = Subject; + if (string.IsNullOrEmpty(subject)) + return; + + var typeface = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Normal); + var foreground = Foreground; + var x = 0.0; + var h = Bounds.Height; + var prefix = null as FormattedText; + + var match = REG_KEYWORD_ON().Match(subject); + if (match.Success) + { + prefix = new FormattedText(match.Groups[1].Value, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 11, foreground); + subject = subject.Substring(match.Length); + } + else + { + match = REG_KEYWORD_WIP().Match(subject); + if (match.Success) + { + prefix = new FormattedText($"WIP | {match.Groups[1].Value}", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 11, foreground); + subject = subject.Substring(match.Length); + } + } + + if (prefix != null) + { + var pw = prefix.WidthIncludingTrailingWhitespace; + var ph = prefix.Height; + var bh = ph + 4; + var bw = pw + 12; + + context.DrawRectangle(PrefixBackground, null, new RoundedRect(new Rect(0, (h - bh) * 0.5, bw, bh), new CornerRadius(bh * 0.5))); + context.DrawText(prefix, new Point(6, (h - ph) * 0.5)); + x = bw + 4; + } + + var body = new FormattedText(subject, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, FontSize, foreground); + context.DrawText(body, new Point(x, (h - body.Height) * 0.5)); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SubjectProperty || + change.Property == FontFamilyProperty || + change.Property == FontSizeProperty || + change.Property == ForegroundProperty || + change.Property == PrefixBackgroundProperty) + { + InvalidateVisual(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var typeface = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Normal); + var test = new FormattedText("fgl|", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, FontSize, Brushes.White); + var h = Math.Max(18, test.Height); + return new Size(availableSize.Width, h); + } + + [GeneratedRegex(@"^On ([^\s]+)\: ")] + private static partial Regex REG_KEYWORD_ON(); + + [GeneratedRegex(@"^WIP on ([^\s]+)\: ([a-f0-9]{6,40}) ")] + private static partial Regex REG_KEYWORD_WIP(); + } +} diff --git a/src/Views/StashesPage.axaml b/src/Views/StashesPage.axaml index 6b087eb8..1c3e2e50 100644 --- a/src/Views/StashesPage.axaml +++ b/src/Views/StashesPage.axaml @@ -19,18 +19,12 @@ - + - - - + + + + @@ -71,6 +65,7 @@ ItemsSource="{Binding VisibleStashes}" SelectedItem="{Binding SelectedStash, Mode=TwoWay}" SelectionMode="Single" + KeyDown="OnStashListKeyDown" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto"> @@ -89,14 +84,23 @@ - + - + @@ -105,10 +109,12 @@ - + - - + + + + diff --git a/src/Views/StashesPage.axaml.cs b/src/Views/StashesPage.axaml.cs index 461f9d5d..d152a12f 100644 --- a/src/Views/StashesPage.axaml.cs +++ b/src/Views/StashesPage.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Input; namespace SourceGit.Views { @@ -23,6 +24,18 @@ namespace SourceGit.Views layout.StashesLeftWidth = new GridLength(maxLeft, GridUnitType.Pixel); } + private void OnStashListKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not (Key.Delete or Key.Back)) + return; + + if (DataContext is not ViewModels.StashesPage vm) + return; + + vm.Drop(vm.SelectedStash); + e.Handled = true; + } + private void OnStashContextRequested(object sender, ContextRequestedEventArgs e) { if (DataContext is ViewModels.StashesPage vm && sender is Border border) diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index 3bbdafbe..163ce031 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -136,9 +136,10 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/SubmodulesView.axaml.cs b/src/Views/SubmodulesView.axaml.cs new file mode 100644 index 00000000..81ccdc5d --- /dev/null +++ b/src/Views/SubmodulesView.axaml.cs @@ -0,0 +1,182 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class SubmoduleTreeNodeToggleButton : ToggleButton + { + protected override Type StyleKeyOverride => typeof(ToggleButton); + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + DataContext is ViewModels.SubmoduleTreeNode { IsFolder: true } node) + { + var view = this.FindAncestorOfType(); + view?.ToggleNodeIsExpanded(node); + } + + e.Handled = true; + } + } + + public class SubmoduleTreeNodeIcon : UserControl + { + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded)); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsExpandedProperty) + UpdateContent(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + UpdateContent(); + } + + private void UpdateContent() + { + if (DataContext is not ViewModels.SubmoduleTreeNode node) + { + Content = null; + return; + } + + if (node.Module != null) + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Submodule"); + else if (node.IsExpanded) + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); + else + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder"); + } + + private void CreateContent(Thickness margin, string iconKey) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + Content = new Avalonia.Controls.Shapes.Path() + { + Width = 12, + Height = 12, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = margin, + Data = geo, + }; + } + } + + public partial class SubmodulesView : UserControl + { + public static readonly RoutedEvent RowsChangedEvent = + RoutedEvent.Register(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler RowsChanged + { + add { AddHandler(RowsChangedEvent, value); } + remove { RemoveHandler(RowsChangedEvent, value); } + } + + public int Rows + { + get; + private set; + } + + public SubmodulesView() + { + InitializeComponent(); + } + + public void ToggleNodeIsExpanded(ViewModels.SubmoduleTreeNode node) + { + if (Content is ViewModels.SubmoduleCollectionAsTree tree) + { + tree.ToggleExpand(node); + Rows = tree.Rows.Count; + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ContentProperty) + { + if (Content is ViewModels.SubmoduleCollectionAsTree tree) + Rows = tree.Rows.Count; + else if (Content is ViewModels.SubmoduleCollectionAsList list) + Rows = list.Submodules.Count; + else + Rows = 0; + + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + else if (change.Property == IsVisibleProperty) + { + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (sender is Control control && DataContext is ViewModels.Repository repo) + { + if (control.DataContext is ViewModels.SubmoduleTreeNode node) + { + if (node.IsFolder) + ToggleNodeIsExpanded(node); + else if (node.Module.Status != Models.SubmoduleStatus.NotInited) + repo.OpenSubmodule(node.Module.Path); + } + else if (control.DataContext is Models.Submodule m && m.Status != Models.SubmoduleStatus.NotInited) + { + repo.OpenSubmodule(m.Path); + } + } + + e.Handled = true; + } + + private void OnItemContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is Control control && DataContext is ViewModels.Repository repo) + { + if (control.DataContext is ViewModels.SubmoduleTreeNode node && node.Module != null) + { + var menu = repo.CreateContextMenuForSubmodule(node.Module); + menu?.Open(control); + } + else if (control.DataContext is Models.Submodule m) + { + var menu = repo.CreateContextMenuForSubmodule(m); + menu?.Open(control); + } + } + + e.Handled = true; + } + } +} diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml index b5384c8f..655d046a 100644 --- a/src/Views/TagsView.axaml +++ b/src/Views/TagsView.axaml @@ -23,39 +23,62 @@ + KeyDown="OnKeyDown" + SelectionChanged="OnSelectionChanged"> + + + + + + + + + + + + + + + + + - - + + + - + - + + + + - - - - - - - - + + + + + + + + + @@ -66,30 +89,47 @@ Margin="8,0,0,0" ItemsSource="{Binding Tags}" SelectionMode="Single" - SelectionChanged="OnRowSelectionChanged"> + KeyDown="OnKeyDown" + SelectionChanged="OnSelectionChanged"> - - + + + + + + + + + + - + + + - - + + + + + + + + - diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs index c83cfd28..1b384262 100644 --- a/src/Views/TagsView.axaml.cs +++ b/src/Views/TagsView.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia; using Avalonia.Controls; @@ -31,15 +30,6 @@ namespace SourceGit.Views public class TagTreeNodeIcon : UserControl { - public static readonly StyledProperty NodeProperty = - AvaloniaProperty.Register(nameof(Node)); - - public ViewModels.TagTreeNode Node - { - get => GetValue(NodeProperty); - set => SetValue(NodeProperty, value); - } - public static readonly StyledProperty IsExpandedProperty = AvaloniaProperty.Register(nameof(IsExpanded)); @@ -49,16 +39,23 @@ namespace SourceGit.Views set => SetValue(IsExpandedProperty, value); } - static TagTreeNodeIcon() + protected override void OnDataContextChanged(EventArgs e) { - NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); - IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + base.OnDataContextChanged(e); + UpdateContent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsExpandedProperty) + UpdateContent(); } private void UpdateContent() { - var node = Node; - if (node == null) + if (DataContext is not ViewModels.TagTreeNode node) { Content = null; return; @@ -92,24 +89,6 @@ namespace SourceGit.Views public partial class TagsView : UserControl { - public static readonly StyledProperty ShowTagsAsTreeProperty = - AvaloniaProperty.Register(nameof(ShowTagsAsTree)); - - public bool ShowTagsAsTree - { - get => GetValue(ShowTagsAsTreeProperty); - set => SetValue(ShowTagsAsTreeProperty, value); - } - - public static readonly StyledProperty> TagsProperty = - AvaloniaProperty.Register>(nameof(Tags)); - - public List Tags - { - get => GetValue(TagsProperty); - set => SetValue(TagsProperty, value); - } - public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); @@ -150,33 +129,7 @@ namespace SourceGit.Views { if (Content is ViewModels.TagCollectionAsTree tree) { - node.IsExpanded = !node.IsExpanded; - - var depth = node.Depth; - var idx = tree.Rows.IndexOf(node); - if (idx == -1) - return; - - if (node.IsExpanded) - { - var subrows = new List(); - MakeTreeRows(subrows, node.Children); - tree.Rows.InsertRange(idx + 1, subrows); - } - else - { - var removeCount = 0; - for (int i = idx + 1; i < tree.Rows.Count; i++) - { - var row = tree.Rows[i]; - if (row.Depth <= depth) - break; - - removeCount++; - } - tree.Rows.RemoveRange(idx + 1, removeCount); - } - + tree.ToggleExpand(node); Rows = tree.Rows.Count; RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } @@ -186,9 +139,15 @@ namespace SourceGit.Views { base.OnPropertyChanged(change); - if (change.Property == ShowTagsAsTreeProperty || change.Property == TagsProperty) + if (change.Property == ContentProperty) { - UpdateDataSource(); + if (Content is ViewModels.TagCollectionAsTree tree) + Rows = tree.Rows.Count; + else if (Content is ViewModels.TagCollectionAsList list) + Rows = list.Tags.Count; + else + Rows = 0; + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } else if (change.Property == IsVisibleProperty) @@ -197,18 +156,30 @@ namespace SourceGit.Views } } - private void OnDoubleTappedNode(object sender, TappedEventArgs e) + private void OnItemDoubleTapped(object sender, TappedEventArgs e) { - if (sender is Grid { DataContext: ViewModels.TagTreeNode node }) - { - if (node.IsFolder) - ToggleNodeIsExpanded(node); - } + if (sender is Control { DataContext: ViewModels.TagTreeNode { IsFolder: true } node }) + ToggleNodeIsExpanded(node); e.Handled = true; } - private void OnRowContextRequested(object sender, ContextRequestedEventArgs e) + private void OnItemPointerPressed(object sender, PointerPressedEventArgs e) + { + var p = e.GetCurrentPoint(this); + if (!p.Properties.IsLeftButtonPressed) + return; + + if (DataContext is not ViewModels.Repository repo) + return; + + if (sender is Control { DataContext: Models.Tag tag }) + repo.NavigateToCommit(tag.SHA); + else if (sender is Control { DataContext: ViewModels.TagTreeNode { Tag: { } nodeTag } }) + repo.NavigateToCommit(nodeTag.SHA); + } + + private void OnItemContextRequested(object sender, ContextRequestedEventArgs e) { var control = sender as Control; if (control == null) @@ -231,7 +202,7 @@ namespace SourceGit.Views e.Handled = true; } - private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) + private void OnSelectionChanged(object sender, SelectionChangedEventArgs _) { var selected = (sender as ListBox)?.SelectedItem; var selectedTag = null as Models.Tag; @@ -240,69 +211,22 @@ namespace SourceGit.Views else if (selected is Models.Tag tag) selectedTag = tag; - if (selectedTag != null && DataContext is ViewModels.Repository repo) - { + if (selectedTag != null) RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); - repo.NavigateToCommit(selectedTag.SHA); - } } - private void MakeTreeRows(List rows, List nodes) + private void OnKeyDown(object sender, KeyEventArgs e) { - foreach (var node in nodes) - { - rows.Add(node); - - if (!node.IsExpanded || !node.IsFolder) - continue; - - MakeTreeRows(rows, node.Children); - } - } - - private void UpdateDataSource() - { - var tags = Tags; - if (tags == null || tags.Count == 0) - { - Rows = 0; - Content = null; + if (DataContext is not ViewModels.Repository repo) return; - } - if (ShowTagsAsTree) - { - var oldExpanded = new HashSet(); - if (Content is ViewModels.TagCollectionAsTree oldTree) - { - foreach (var row in oldTree.Rows) - { - if (row.IsFolder && row.IsExpanded) - oldExpanded.Add(row.FullPath); - } - } + var selected = (sender as ListBox)?.SelectedItem; + if (selected is ViewModels.TagTreeNode { Tag: { } tagInNode }) + repo.DeleteTag(tagInNode); + else if (selected is Models.Tag tag) + repo.DeleteTag(tag); - var tree = new ViewModels.TagCollectionAsTree(); - tree.Tree = ViewModels.TagTreeNode.Build(tags, oldExpanded); - - var rows = new List(); - MakeTreeRows(rows, tree.Tree); - tree.Rows.AddRange(rows); - - Content = tree; - Rows = rows.Count; - } - else - { - var list = new ViewModels.TagCollectionAsList(); - list.Tags.AddRange(tags); - - Content = list; - Rows = tags.Count; - } - - RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + e.Handled = true; } } } - diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 7ce9f288..e08554e9 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -126,7 +126,7 @@ namespace SourceGit.Views typeface, presenter.FontSize, presenter.Foreground); - context.DrawText(txt, new Point(Bounds.Width - txt.Width, y - txt.Height * 0.5)); + context.DrawText(txt, new Point(Bounds.Width - txt.Width, y - (txt.Height * 0.5))); } } } @@ -212,7 +212,7 @@ namespace SourceGit.Views } if (indicator != null) - context.DrawText(indicator, new Point(0, y - indicator.Height * 0.5)); + context.DrawText(indicator, new Point(0, y - (indicator.Height * 0.5))); } } } @@ -348,16 +348,11 @@ namespace SourceGit.Views private ThemedTextDiffPresenter _presenter = null; } - public class LineStyleTransformer : DocumentColorizingTransformer + public class LineStyleTransformer(ThemedTextDiffPresenter presenter) : DocumentColorizingTransformer { - public LineStyleTransformer(ThemedTextDiffPresenter presenter) - { - _presenter = presenter; - } - protected override void ColorizeLine(DocumentLine line) { - var lines = _presenter.GetLines(); + var lines = presenter.GetLines(); var idx = line.LineNumber; if (idx > lines.Count) return; @@ -367,13 +362,11 @@ namespace SourceGit.Views { ChangeLinePart(line.Offset, line.EndOffset, v => { - v.TextRunProperties.SetForegroundBrush(_presenter.IndicatorForeground); - v.TextRunProperties.SetTypeface(new Typeface(_presenter.FontFamily, FontStyle.Italic)); + v.TextRunProperties.SetForegroundBrush(presenter.IndicatorForeground); + v.TextRunProperties.SetTypeface(new Typeface(presenter.FontFamily, FontStyle.Italic)); }); } } - - private readonly ThemedTextDiffPresenter _presenter; } public static readonly StyledProperty FileNameProperty = @@ -745,6 +738,7 @@ namespace SourceGit.Views var val = ShowHiddenSymbols; Options.ShowTabs = val; Options.ShowSpaces = val; + Options.ShowEndOfLine = val; } else if (change.Property == TabWidthProperty) { @@ -764,12 +758,10 @@ namespace SourceGit.Views } else if (change.Property == BlockNavigationProperty) { - var oldValue = change.OldValue as ViewModels.BlockNavigation; - if (oldValue != null) + if (change.OldValue is ViewModels.BlockNavigation oldValue) oldValue.PropertyChanged -= OnBlockNavigationPropertyChanged; - var newValue = change.NewValue as ViewModels.BlockNavigation; - if (newValue != null) + if (change.NewValue is ViewModels.BlockNavigation newValue) newValue.PropertyChanged += OnBlockNavigationPropertyChanged; TextArea?.TextView?.Redraw(); @@ -1046,7 +1038,8 @@ namespace SourceGit.Views // The first selected line (partial selection) if (i == startIdx && startPosition.Column > 1) { - builder.AppendLine(line.Content.Substring(startPosition.Column - 1)); + builder.Append(line.Content.AsSpan(startPosition.Column - 1)); + builder.Append(Environment.NewLine); continue; } @@ -1060,7 +1053,14 @@ namespace SourceGit.Views // For the last line (selection range is within original source) if (i == endIdx) { - builder.Append(endPosition.Column - 1 < line.Content.Length ? line.Content.Substring(0, endPosition.Column - 1) : line.Content); + if (endPosition.Column - 1 < line.Content.Length) + { + builder.Append(line.Content.AsSpan(0, endPosition.Column - 1)); + } + else + { + builder.Append(line.Content); + } break; } @@ -1242,22 +1242,25 @@ namespace SourceGit.Views { base.OnDataContextChanged(e); - var textDiff = DataContext as Models.TextDiff; - if (textDiff != null) + if (DataContext is Models.TextDiff textDiff) { var builder = new StringBuilder(); foreach (var line in textDiff.Lines) { if (line.Content.Length > 10000) { - builder.Append(line.Content.Substring(0, 1000)); + builder.Append(line.Content.AsSpan(0, 1000)); builder.Append($"...({line.Content.Length - 1000} character trimmed)"); - builder.AppendLine(); } else { - builder.AppendLine(line.Content); + builder.Append(line.Content); } + + if (line.NoNewLineEndOfFile) + builder.Append("\u26D4"); + + builder.Append('\n'); } Text = builder.ToString(); @@ -1397,8 +1400,7 @@ namespace SourceGit.Views return; } - var textDiff = this.FindAncestorOfType()?.DataContext as Models.TextDiff; - if (textDiff != null) + if (this.FindAncestorOfType()?.DataContext is Models.TextDiff textDiff) { var lineIdx = -1; foreach (var line in view.VisualLines) @@ -1489,14 +1491,18 @@ namespace SourceGit.Views { if (line.Content.Length > 10000) { - builder.Append(line.Content.Substring(0, 1000)); + builder.Append(line.Content.AsSpan(0, 1000)); builder.Append($"...({line.Content.Length - 1000} characters trimmed)"); - builder.AppendLine(); } else { - builder.AppendLine(line.Content); + builder.Append(line.Content); } + + if (line.NoNewLineEndOfFile) + builder.Append("\u26D4"); + + builder.Append('\n'); } Text = builder.ToString(); @@ -1520,7 +1526,7 @@ namespace SourceGit.Views private void DirectSyncScrollOffset() { - if (_scrollViewer is { } && DataContext is ViewModels.TwoSideTextDiff diff) + if (_scrollViewer is not null && DataContext is ViewModels.TwoSideTextDiff diff) diff.SyncScrollOffset = _scrollViewer?.Offset ?? Vector.Zero; } @@ -1731,8 +1737,8 @@ namespace SourceGit.Views return; } - var top = chunk.Y + (chunk.Height >= 36 ? 16 : 4); - var right = (chunk.Combined || !chunk.IsOldSide) ? 16 : v.Bounds.Width * 0.5f + 16; + var top = chunk.Y + (chunk.Height >= 36 ? 8 : 2); + var right = (chunk.Combined || !chunk.IsOldSide) ? 26 : (v.Bounds.Width * 0.5f) + 26; v.Popup.Margin = new Thickness(0, top, right, 0); v.Popup.IsVisible = true; }); @@ -1851,7 +1857,7 @@ namespace SourceGit.Views if (!selection.HasLeftChanges) { - new Commands.Add(repo.FullPath, [change.Path]).Exec(); + new Commands.Add(repo.FullPath, change).Exec(); } else { @@ -1912,7 +1918,7 @@ namespace SourceGit.Views if (change.DataForAmend != null) new Commands.UnstageChangesForAmend(repo.FullPath, [change]).Exec(); else - new Commands.Reset(repo.FullPath, [change]).Exec(); + new Commands.Restore(repo.FullPath, change).Exec(); } else { @@ -1963,7 +1969,7 @@ namespace SourceGit.Views if (!selection.HasLeftChanges) { - Commands.Discard.Changes(repo.FullPath, [change]); + Commands.Discard.Changes(repo.FullPath, [change], null); } else { diff --git a/src/Views/ViewLogs.axaml b/src/Views/ViewLogs.axaml new file mode 100644 index 00000000..a3b9e240 --- /dev/null +++ b/src/Views/ViewLogs.axaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +