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/.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 50d00f58..f9ba3072 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ * Search commits * GitFlow * Git LFS +* Bisect * Issue Link * Workspace * Custom Action @@ -53,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. @@ -92,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 866aac65..1fa3259d 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,145 +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-96.19%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-%E2%88%9A-brightgreen) -
-Missing keys in de_DE.axaml - -- Text.BranchUpstreamInvalid -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitMessageTextBox.SubjectCount -- 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.Repository.ViewLogs -- Text.StashCM.SaveAsPatch -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs - -
- -### ![es__ES](https://img.shields.io/badge/es__ES-98.95%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.13%25-yellow)
Missing keys in es_ES.axaml -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.Repository.ViewLogs -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete +- 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-97.51%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-97.24%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-96.53%25-yellow)
Missing keys in it_IT.axaml -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitMessageTextBox.SubjectCount -- Text.Configure.Git.PreferredMergeMode -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.CopyFullPath -- Text.Preferences.General.ShowTagsInGraph -- Text.Repository.ViewLogs -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs +- Text.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-97.24%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-88.71%25-yellow) +### ![pt__BR](https://img.shields.io/badge/pt__BR-83.25%25-yellow)
Missing keys in pt_BR.axaml @@ -155,15 +230,29 @@ 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 @@ -178,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 @@ -201,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 @@ -209,94 +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-98.82%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) -
-Missing keys in ru_RU.axaml - -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitMessageTextBox.SubjectCount -- Text.Repository.ViewLogs -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete - -
- -### ![ta__IN](https://img.shields.io/badge/ta__IN-97.51%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-98.69%25-yellow) +### ![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 89f9e970..11692c6b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.14 \ 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/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 73b97017..186022d5 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -35,7 +35,7 @@ - + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index c659388a..6b42700d 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -78,7 +78,7 @@ namespace SourceGit return builder; } - private static void LogException(Exception ex) + public static void LogException(Exception ex) { if (ex == null) return; @@ -105,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) @@ -267,7 +301,7 @@ namespace SourceGit return await clipboard.GetTextAsync(); } } - return default; + return null; } public static string Text(string key, params object[] args) @@ -289,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; @@ -304,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; } @@ -342,6 +375,14 @@ 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; @@ -351,7 +392,17 @@ namespace SourceGit _ipcChannel = new Models.IpcChannel(); if (!_ipcChannel.IsFirstInstance) { - _ipcChannel.SendToFirstInstance(desktop.Args is { Length: 1 } ? desktop.Args[0] : string.Empty); + 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 @@ -445,7 +496,7 @@ namespace SourceGit if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) return true; - var done = File.ReadAllText(doneFile).Trim().Split([ '\r', '\n' ], StringSplitOptions.RemoveEmptyEntries); + var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); if (done.Length == 0) return true; @@ -470,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]; @@ -480,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; } @@ -506,7 +557,7 @@ namespace SourceGit private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) { - Native.OS.SetupEnternalTools(); + Native.OS.SetupExternalTools(); Models.AvatarManager.Instance.Start(); string startupRepo = null; @@ -518,6 +569,7 @@ namespace SourceGit _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; #if !DISABLE_UPDATE_DETECTION if (pref.ShouldCheck4UpdateOnStartup()) @@ -598,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); }); } @@ -632,7 +680,15 @@ 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; 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/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 0bb45c98..1fc51fa4 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -51,7 +51,7 @@ namespace SourceGit.Commands private void ParseLine(string line) { - if (line.IndexOf('\0', StringComparison.Ordinal) >= 0) + if (line.Contains('\0', StringComparison.Ordinal)) { _result.IsBinary = true; _result.LineInfos.Clear(); @@ -89,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 9c396215..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,12 +13,20 @@ return cmd.ReadToEnd().StdOut.Trim(); } - public static bool Create(string repo, string name, string basedOn, Models.ICommandLog log) + 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(); } diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index c39c28ae..d2876740 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -11,15 +11,37 @@ namespace SourceGit.Commands Context = repo; } - public bool Branch(string branch) + public bool Branch(string branch, bool force) { - Args = $"checkout --recurse-submodules --progress {branch}"; + 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) + public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite) { - Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}"; + 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(); } @@ -56,11 +78,5 @@ namespace SourceGit.Commands Args = $"checkout --no-overlay {revision} -- \"{file}\""; return Exec(); } - - public bool Commit(string commitId) - { - Args = $"checkout --detach --progress {commitId}"; - return Exec(); - } } } 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/Command.cs b/src/Commands/Command.cs index 699cc120..975922fc 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -36,44 +36,14 @@ namespace SourceGit.Commands public bool Exec() { + Log?.AppendLine($"$ git {Args}\n"); + var start = CreateGitStartInfo(); var errs = new List(); var proc = new Process() { StartInfo = start }; - Log?.AppendLine($"$ git {Args}\n"); - - proc.OutputDataReceived += (_, e) => - { - if (e.Data == null) - return; - - Log?.AppendLine(e.Data); - }; - - proc.ErrorDataReceived += (_, e) => - { - if (string.IsNullOrEmpty(e.Data)) - { - errs.Add(string.Empty); - return; - } - - Log?.AppendLine(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(); @@ -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 5be08cef..1585e7e3 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,7 +4,7 @@ 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); @@ -12,10 +12,10 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; 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() @@ -34,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 7b4a496d..c88e087a 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -39,7 +39,7 @@ namespace SourceGit.Commands foreach (var line in lines) ParseLine(line); - _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal)); + _changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); return _changes; } 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 65a2a6f5..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,9 +28,11 @@ 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 --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --unified={unified} {opt}"; } public Models.DiffResult Result() @@ -103,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 == '+') @@ -114,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; } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index f837df52..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 { + /// + /// Discard all local changes (unstaged & staged) + /// + /// + /// + /// public static void All(string repo, bool includeIgnored, Models.ICommandLog log) { - new Restore(repo) { Log = log }.Exec(); - new Clean(repo, includeIgnored) { Log = log }.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(); } + /// + /// 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)) { Log = log }.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") { Log = log }.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/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/GitFlow.cs b/src/Commands/GitFlow.cs index 833d268d..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, Models.ICommandLog log) - { - 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, log); - - var devBranch = branches.Find(x => x.Name == develop); - if (devBranch == null && current != null) - Branch.Create(repo, develop, current.Head, log); - var config = new Config(repo); config.Set("gitflow.branch.master", master); config.Set("gitflow.branch.develop", develop); @@ -65,103 +25,68 @@ namespace SourceGit.Commands 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, Models.ICommandLog log) - { - 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, Models.ICommandLog log) + 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/LFS.cs b/src/Commands/LFS.cs index e621ed7d..18d2ba93 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -10,7 +10,7 @@ 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, Models.ICommandLog log) { diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs index b08377b9..32898593 100644 --- a/src/Commands/Merge.cs +++ b/src/Commands/Merge.cs @@ -5,11 +5,20 @@ namespace SourceGit.Commands { public class Merge : Command { - public Merge(string repo, string source, string mode) + public Merge(string repo, string source, string mode, bool edit) { WorkingDirectory = repo; Context = repo; - 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) 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 2695b16b..698fbfce 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -2,7 +2,7 @@ { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase, bool noTags) + public Pull(string repo, string remote, string branch, bool useRebase) { WorkingDirectory = repo; Context = repo; @@ -12,9 +12,6 @@ if (useRebase) Args += "--rebase=true "; - if (noTags) - Args += "--no-tags "; - Args += $"{remote} {branch}"; } } diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 39b77189..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.IsUpstreamGone = !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.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/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 4e626a79..788ed617 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Text.RegularExpressions; +using Avalonia.Threading; + namespace SourceGit.Commands { public partial class QueryLocalChanges : Command @@ -22,7 +24,10 @@ namespace SourceGit.Commands var outs = new List(); var rs = ReadToEnd(); if (!rs.IsSuccess) + { + Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr)); return outs; + } var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) @@ -117,37 +122,36 @@ namespace SourceGit.Commands 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); + change.ConflictReason = Models.ConflictReason.BothDeleted; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "AU": - change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged); + change.ConflictReason = Models.ConflictReason.AddedByUs; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "UD": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted); + change.ConflictReason = Models.ConflictReason.DeletedByThem; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "UA": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added); + change.ConflictReason = Models.ConflictReason.AddedByThem; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "DU": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged); + change.ConflictReason = Models.ConflictReason.DeletedByUs; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "AA": - change.Set(Models.ChangeState.Added, Models.ChangeState.Added); + change.ConflictReason = Models.ConflictReason.BothAdded; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "UU": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged); + change.ConflictReason = Models.ConflictReason.BothModified; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); break; case "??": - change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked); - break; - default: + change.Set(Models.ChangeState.None, Models.ChangeState.Untracked); break; } 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 b4067aaf..2b8987b6 100644 --- a/src/Commands/QueryStashes.cs +++ b/src/Commands/QueryStashes.cs @@ -9,7 +9,7 @@ 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() @@ -19,55 +19,50 @@ namespace SourceGit.Commands if (!rs.IsSuccess) return outs; - var nextPartIdx = 0; - var start = 0; - var end = rs.StdOut.IndexOf('\n', start); - while (end > 0) + var items = rs.StdOut.Split('\0', StringSplitOptions.RemoveEmptyEntries); + foreach (var item in items) { - var line = rs.StdOut.Substring(start, end - start); + var current = new Models.Stash(); - switch (nextPartIdx) + var nextPartIdx = 0; + var start = 0; + var end = item.IndexOf('\n', start); + while (end > 0 && nextPartIdx < 4) { - case 0: - _current = new Models.Stash() { SHA = line }; - outs.Add(_current); - break; - case 1: - ParseParent(line); - break; - case 2: - _current.Time = ulong.Parse(line); - break; - case 3: - _current.Name = line; - break; - case 4: - _current.Message = line; + 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); } - nextPartIdx++; - if (nextPartIdx > 4) - nextPartIdx = 0; + if (start < item.Length) + current.Message = item.Substring(start); - start = end + 1; - end = rs.StdOut.IndexOf('\n', start); + outs.Add(current); } - - if (start < rs.StdOut.Length) - _current.Message = rs.StdOut.Substring(start); - return outs; } - - private void ParseParent(string data) - { - if (data.Length < 8) - return; - - _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); - } - - private Models.Stash _current = null; } } 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 ea0b86de..e11c1740 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -40,7 +40,7 @@ namespace SourceGit.Commands if (dateEndIdx == -1) return; - var dateStr = line.AsSpan().Slice(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 e4f35fca..025d035a 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -1,4 +1,7 @@ -namespace SourceGit.Commands +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { public class Submodule : Command { @@ -10,7 +13,7 @@ public bool Add(string url, string relativePath, bool recursive) { - Args = $"submodule add {url} \"{relativePath}\""; + Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\""; if (!Exec()) return false; @@ -26,29 +29,37 @@ } } - public bool Update(string module, bool init, bool recursive, bool useRemote) + 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}\""); + } + 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 = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\""; + return Exec(); + } - Args = $"rm -rf \"{relativePath}\""; + public bool Delete(string module) + { + Args = $"rm -rf \"{module}\""; return Exec(); } } diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index 6fc8dc34..017afea0 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -9,7 +9,7 @@ namespace SourceGit.Commands 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(); } @@ -28,12 +28,13 @@ namespace SourceGit.Commands 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(); } 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 1e7bb239..00000000 --- a/src/Commands/UpdateRef.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SourceGit.Commands -{ - public class UpdateRef : Command - { - public UpdateRef(string repo, string refName, string toRevision) - { - WorkingDirectory = repo; - Context = repo; - Args = $"update-ref {refName} {toRevision}"; - } - } -} 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 d0ac1990..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; } 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 cd09453a..531a16c0 100644 --- a/src/Models/ConventionalCommitType.cs +++ b/src/Models/ConventionalCommitType.cs @@ -4,25 +4,24 @@ namespace SourceGit.Models { public class ConventionalCommitType { - public string Name { get; set; } = string.Empty; - 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("Features", "feat", "Adding a new feature"), - new ConventionalCommitType("Bug Fixes", "fix", "Fixing a bug"), - new ConventionalCommitType("Work In Progress", "wip", "Still being developed and not yet complete"), - new ConventionalCommitType("Reverts", "revert", "Undoing a previous commit"), - new ConventionalCommitType("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"), - new ConventionalCommitType("Performance Improvements", "pref", "Improves performance"), - new ConventionalCommitType("Builds", "build", "Changes that affect the build system or external dependencies"), - new ConventionalCommitType("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"), - new ConventionalCommitType("Documentations", "docs", "Updating documentation"), - new ConventionalCommitType("Styles", "style", "Elements or code styles without changing the code logic"), - new ConventionalCommitType("Tests", "test", "Adding or updating tests"), - new ConventionalCommitType("Chores", "chore", "Other changes that don't modify src or test files"), - }; + public static readonly List Supported = [ + new("Features", "feat", "Adding a new feature"), + new("Bug Fixes", "fix", "Fixing a bug"), + new("Work In Progress", "wip", "Still being developed and not yet complete"), + new("Reverts", "revert", "Undoing a previous commit"), + new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"), + new("Performance Improvements", "perf", "Improves performance"), + new("Builds", "build", "Changes that affect the build system or external dependencies"), + new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"), + new("Documentations", "docs", "Updating documentation"), + new("Styles", "style", "Elements or code styles without changing the code logic"), + new("Tests", "test", "Adding or updating tests"), + new("Chores", "chore", "Other changes that don't modify src or test files"), + ]; public ConventionalCommitType(string name, string type, string description) { 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 e7d17994..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 @@ -147,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'); @@ -556,7 +555,7 @@ namespace SourceGit.Models } else if (test.Type == TextDiffLineType.Added) { - if (i < start - 1) + if (i < start - 1 || isOldSide) { if (revert) { @@ -566,18 +565,7 @@ namespace SourceGit.Models } else { - if (isOldSide) - { - if (revert) - { - newCount++; - oldCount++; - } - } - else - { - newCount++; - } + newCount++; } if (i == end - 1 && tailed) @@ -655,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/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/IpcChannel.cs b/src/Models/IpcChannel.cs index 2ecfb771..001c65a6 100644 --- a/src/Models/IpcChannel.cs +++ b/src/Models/IpcChannel.cs @@ -8,10 +8,7 @@ namespace SourceGit.Models { public class IpcChannel : IDisposable { - public bool IsFirstInstance - { - get => _isFirstInstance; - } + public bool IsFirstInstance { get; } public event Action MessageReceived; @@ -19,10 +16,10 @@ namespace SourceGit.Models { try { - _singletoneLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); - _isFirstInstance = true; + _singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + IsFirstInstance = true; _server = new NamedPipeServerStream( - "SourceGitIPCChannel", + "SourceGitIPCChannel" + Environment.UserName, PipeDirection.In, -1, PipeTransmissionMode.Byte, @@ -32,7 +29,7 @@ namespace SourceGit.Models } catch { - _isFirstInstance = false; + IsFirstInstance = false; } } @@ -40,7 +37,7 @@ namespace SourceGit.Models { try { - using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel", PipeDirection.Out)) + using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) { client.Connect(1000); if (!client.IsConnected) @@ -67,7 +64,7 @@ namespace SourceGit.Models public void Dispose() { _cancellationTokenSource?.Cancel(); - _singletoneLock?.Dispose(); + _singletonLock?.Dispose(); } private async void StartServer() @@ -96,8 +93,7 @@ namespace SourceGit.Models } } - private FileStream _singletoneLock = null; - private bool _isFirstInstance = false; + 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/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 34a72033..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; @@ -281,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; @@ -326,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) { @@ -399,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; @@ -446,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 d982a3ed..a86380c3 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -26,25 +26,23 @@ namespace SourceGit.Models 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(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 == StatisticsMode.ThisWeek) { @@ -72,7 +70,7 @@ namespace SourceGit.Models { Total++; - var normalized = DateTime.MinValue; + DateTime normalized; if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth) normalized = time.Date; else @@ -172,26 +170,27 @@ namespace SourceGit.Models ChangeColor(_fillColor); } - private StatisticsMode _mode = StatisticsMode.All; - private Dictionary _mapUsers = new Dictionary(); - private Dictionary _mapSamples = new Dictionary(); - private Dictionary> _mapUserSamples = new Dictionary>(); + 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(StatisticsMode.All, DateTime.MinValue); Month = new StatisticsReport(StatisticsMode.ThisMonth, _thisMonthStart); @@ -200,7 +199,13 @@ namespace SourceGit.Models 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) @@ -214,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 51e3d8bf..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 @@ -87,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 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 7a3da294..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 + 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 @@ -178,6 +200,10 @@ 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: @@ -187,6 +213,7 @@ Typ der Änderung: Kopieren Kopiere gesamten Text + Ganzen Pfad kopieren Pfad kopieren Branch erstellen... Basierend auf: @@ -198,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 @@ -212,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!!! @@ -236,7 +267,9 @@ ALT Kopieren Dateimodus geändert - Ignoriere Leerzeichenänderungen und EOL + Erste Differenz + Ignoriere Leerzeichenänderungen + Letzte Differenz LFS OBJEKT ÄNDERUNG Nächste Änderung KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN @@ -245,6 +278,7 @@ Zeige versteckte Symbole Nebeneinander SUBMODUL + GELÖSCHT NEU Seiten wechseln Syntax Hervorhebung @@ -269,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 @@ -304,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 @@ -365,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 @@ -385,6 +422,7 @@ Suchpanel schließen Suche nächste Übereinstimmung Suche vorherige Übereinstimmung + Öffne mit externem Diff/Merge Tool Öffne Suchpanel Verwerfen Stagen @@ -406,7 +444,10 @@ In Browser öffnen FEHLER INFO + Arbeitsplätze + Tabs Branch mergen + Merge-Nachricht anpassen Ziel-Branch: Merge Option: Quelle: @@ -439,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 @@ -447,8 +489,10 @@ Modell Name Server + Streaming aktivieren DARSTELLUNG Standardschriftart + Editor Tab Breite Schriftgröße Standard Texteditor @@ -469,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 @@ -476,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 @@ -500,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 @@ -514,6 +559,8 @@ Erzwinge Push Lokaler Branch: Remote: + Revision: + Push Revision zu Remote-Branch Push Remote-Branch: Remote-Branch verfolgen @@ -527,7 +574,6 @@ Lokale Änderungen stashen & wieder anwenden Auf: Rebase: - Aktualisieren Remote hinzufügen Remote bearbeiten Name: @@ -549,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 @@ -583,10 +634,12 @@ Commit suchen Autor Committer + Inhalt Dateiname Commit-Nachricht SHA Aktueller Branch + Submodule als Baum anzeigen Zeige Tags als Baum ÜBERSPRINGEN Statistiken @@ -596,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 @@ -609,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... @@ -640,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 @@ -667,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 @@ -685,6 +748,10 @@ Submodul: Verwende `--remote` Option URL: + Logs + ALLES LÖSCHEN + Kopieren + Löschen Warnung Willkommensseite Erstelle Gruppe @@ -704,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. @@ -714,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 @@ -727,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 20a70f22..d4b4d26f 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -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,6 +64,7 @@ Push ${0}$ Rebase ${0}$ on ${1}$... Rename ${0}$... + Reset ${0}$ to ${1}$... Set Tracking Branch... Branch Compare Invalid upstream! @@ -75,7 +84,10 @@ Local Changes: Discard Stash & Reapply + Update all submodules Branch: + Checkout & Fast-Forward + Fast-Forward to: Cherry Pick Append source to commit message Commit(s): @@ -108,6 +120,7 @@ Interactively Rebase ${0}$ on Here Merge to ${0}$ Merge ... + Push ${0}$ to ${1}$ Rebase ${0}$ on Here Reset ${0}$ to Here Revert Commit @@ -116,6 +129,7 @@ Squash into Parent Squash Children into Here CHANGES + changed file(s) Search Changes... FILES LFS File @@ -207,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 @@ -221,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!!! @@ -246,7 +264,7 @@ Copy File Mode Changed First Difference - Ignore Whitespace Change and EOL + Ignore All Whitespace Changes Last Difference LFS OBJECT CHANGE Next Difference @@ -256,6 +274,7 @@ Show hidden symbols Side-By-Side Diff SUBMODULE + DELETED NEW Swap Syntax Highlighting @@ -280,7 +299,6 @@ Edit Selected Repository Run Custom Action Action Name: - Fast-Forward (without checkout) Fetch Fetch all remotes Force override local refs @@ -315,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 @@ -376,6 +396,8 @@ 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 @@ -396,6 +418,7 @@ Close search panel Find next match Find previous match + Open with external diff/merge tool Open search panel Discard Stage @@ -417,7 +440,10 @@ Open in Browser ERROR NOTICE + Workspaces + Pages Merge Branch + Customize merge message Into: Merge Option: Source: @@ -450,6 +476,7 @@ {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 @@ -490,7 +517,8 @@ User Email Global git user email Enable --prune on fetch - Git (>= 2.23.0) is required by this app + Enable --ignore-cr-at-eol in diff + Git (>= 2.25.1) is required by this app Install Path Enable HTTP SSL Verify User Name @@ -514,12 +542,11 @@ 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 @@ -528,6 +555,8 @@ Force push Local Branch: Remote: + Revision: + Push Revision To Remote Push Changes To Remote Remote Branch: Set as tracking branch @@ -541,7 +570,6 @@ Stash & reapply local changes On: Rebase: - Refresh Add Remote Edit Remote Name: @@ -563,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 @@ -593,43 +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... @@ -655,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 @@ -683,11 +719,18 @@ 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 @@ -724,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. @@ -744,6 +787,7 @@ 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 @@ -752,7 +796,7 @@ UNSTAGED STAGE STAGE ALL - VIEW ASSUME UNCHANGED + VIEW ASSUME UNCHANGED Template: ${0}$ WORKSPACE: Configure Workspaces... diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 204cfc1a..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 + 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 @@ -208,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 @@ -222,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! @@ -247,7 +267,7 @@ Copiar Modo de Archivo Cambiado Primera Diferencia - Ignorar Cambio de Espacios en Blanco y EOL + Ignorar Cambio de Espacios en Blanco Última Diferencia CAMBIO DE OBJETO LFS Siguiente Diferencia @@ -257,6 +277,7 @@ Mostrar símbolos ocultos Diferencia Lado a Lado SUBMÓDULO + BORRADO NUEVO Intercambiar Resaltado de Sintaxis @@ -281,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' @@ -316,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 @@ -377,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 @@ -397,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 @@ -418,6 +443,8 @@ Abrir en el Navegador ERROR AVISO + Espacios de trabajo + Páginas Merge Rama En: Opción de Merge: @@ -451,6 +478,7 @@ Hace {0} meses Hace {0} años Ayer + Usa 'Shift+Enter' para introducir una nueva línea. 'Enter' es el atajo del botón OK Preferencias OPEN AI Analizar Diff Prompt @@ -491,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 @@ -515,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 @@ -542,7 +570,6 @@ Stash & reaplicar cambios locales En: Rebase: - Refrescar Añadir Remoto Editar Remoto Nombre: @@ -564,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 @@ -598,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 @@ -611,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 @@ -624,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... @@ -655,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 @@ -683,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 @@ -701,6 +742,10 @@ Submódulo: Usar opción --remote URL: + Logs + LIMPIAR TODO + Copiar + Borrar Advertencia Página de Bienvenida Crear Grupo @@ -720,7 +765,6 @@ Git Ignore Ignorar todos los archivos *{0} Ignorar archivos *{0} en la misma carpeta - Ignorar archivos en la misma carpeta Ignorar solo este archivo Enmendar Puedes hacer stage a este archivo ahora. @@ -740,6 +784,7 @@ 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 @@ -748,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 f45a83d9..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}$ @@ -241,7 +241,7 @@ Copier Mode de fichier changé Première différence - Ignorer les changements d'espaces et EOL + Ignorer les changements d'espaces Dernière différence CHANGEMENT D'OBJET LFS Différence suivante @@ -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 @@ -485,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 @@ -509,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 @@ -536,7 +534,6 @@ Stash & réappliquer changements locaux Sur : Rebase : - Rafraîchir Ajouter dépôt distant Modifier dépôt distant Nom : @@ -565,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 @@ -605,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 @@ -623,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... @@ -649,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 @@ -714,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. @@ -737,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 b7d2568a..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 + 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 @@ -181,6 +194,10 @@ 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: @@ -190,6 +207,7 @@ Tipo di Modifica: Copia Copia Tutto il Testo + Copia Intero Percorso Copia Percorso Crea Branch... Basato Su: @@ -240,7 +258,7 @@ Copia Modalità File Modificata Prima differenza - Ignora Modifiche agli Spazi e EOL + Ignora Modifiche agli Spazi Ultima differenza MODIFICA OGGETTO LFS Differenza Successiva @@ -274,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 @@ -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,6 +586,7 @@ CONTINUA Azioni Personalizzate Nessuna Azione Personalizzata + Scarta tutte le modifiche Abilita opzione '--reflog' Apri nell'Esplora File Cerca Branch/Tag/Sottomodulo @@ -589,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 @@ -603,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 @@ -621,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... @@ -647,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 @@ -680,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 @@ -693,6 +722,10 @@ Sottomodulo: Usa opzione --remote URL: + Log + CANCELLA TUTTO + Copia + Elimina Avviso Pagina di Benvenuto Crea Gruppo @@ -712,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. @@ -722,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 @@ -735,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 c50504e1..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}$ @@ -241,7 +240,7 @@ コピー ファイルモードが変更されました 先頭の差分 - 空白の変更とEOLを無視 + 空白の変更を無視 最後の差分 LFSオブジェクトの変更 次の差分 @@ -275,7 +274,6 @@ 選択中のリポジトリを編集 カスタムアクションを実行 アクション名: - (チェックアウトせずに)ブランチを早送りする フェッチ すべてのリモートをフェッチ ローカル参照を強制的に上書き @@ -445,6 +443,7 @@ {0} ヶ月前 {0} 年前 昨日 + 改行には'Shift+Enter'キーを使用します。 'Enter"はOKボタンのホットキーとして機能します。 設定 AI 差分分析プロンプト @@ -485,7 +484,7 @@ ユーザー Eメールアドレス グローバルgitのEメールアドレス フェッチ時に--pruneを有効化 - Git (>= 2.23.0) はこのアプリで必要です + Git (>= 2.25.1) はこのアプリで必要です インストール パス HTTP SSL 検証を有効にする ユーザー名 @@ -509,12 +508,10 @@ `$GIT_DIR/worktrees` の作業ツリー情報を削除 プル ブランチ: - すべてのブランチをフェッチ 宛先: ローカルの変更: 破棄 スタッシュして再適用 - タグなしでフェッチ リモート: プル (フェッチ & マージ) マージの代わりにリベースを使用 @@ -536,7 +533,6 @@ ローカルの変更をスタッシュして再適用 On: リベース: - 更新 リモートを追加 リモートを編集 名前: @@ -565,6 +561,7 @@ 続ける カスタムアクション カスタムアクションがありません + すべての変更を破棄 `--reflog` オプションを有効化 ファイルブラウザーで開く ブランチ/タグ/サブモジュールを検索 @@ -604,7 +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 a4e9d883..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}$ @@ -217,7 +217,7 @@ ANTIGO Copiar Modo de Arquivo Alterado - Ignorar mudanças de espaço em branco e EOL + Ignorar mudanças de espaço em branco MUDANÇA DE OBJETO LFS Próxima Diferença SEM MUDANÇAS OU APENAS MUDANÇAS DE EOL @@ -249,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 @@ -407,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 @@ -442,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 @@ -465,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 @@ -492,7 +490,6 @@ Guardar & reaplicar alterações locais Em: Rebase: - Atualizar Adicionar Remoto Editar Remoto Nome: @@ -521,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 @@ -566,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... @@ -588,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 @@ -649,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. @@ -670,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 b8c86415..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 + Субъект Пользовательское действие Интерактивное перемещение (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,6 +225,7 @@ Введите имя ветки. Пробелы будут заменены на тире. Создать локальную ветку + Перезаписать существующую ветку Создать метку... Новая метка у: GPG подпись @@ -221,6 +240,9 @@ Простой Удерживайте Ctrl, чтобы сразу начать Вырезать + Удалить подмодуль + Принудительно удалить даже если содержит локальные изменения. + Подмодуль: Удалить ветку Ветка: Вы собираетесь удалить внешнюю ветку!!! @@ -240,22 +262,23 @@ Удалить метку Метка: Удалить из внешнего репозитория - РАЗНИЦА БИНАРНИКОВ + СРАВНЕНИЕ БИНАРНИКОВ НОВЫЙ СТАРЫЙ Копировать Режим файла изменён - Первое различие - Игнорировать изменение пробелов и EOL - Последнее различие + Первое сравнение + Игнорировать изменения пробелов + Последнее сравнение ИЗМЕНЕНИЕ ОБЪЕКТА 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 Открыть в файловом менеджере Поиск веток, меток и подмодулей @@ -597,10 +634,12 @@ Поиск ревизии Автор Ревизор + Содержимое Файл Сообщение SHA Текущая ветка + Показывать подмодули как дерево Показывать метки как катлог ПРОПУСТИТЬ Статистикa @@ -610,11 +649,12 @@ МЕТКИ НОВАЯ МЕТКА По дате создания - По имени (по возрастанию) - По имени (по убыванию) + По имени Сортировать Открыть в терминале Использовать относительное время в историях + Просмотр журналов + Посетить '{0}' в браузере РАБОЧИЕ КАТАЛОГИ ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ ОБРЕЗАТЬ @@ -623,12 +663,14 @@ Режим сброса: Переместить в: Текущая ветка: + Сброс ветки (без переключения) + Переместить в: + Ветка: Открыть в файловом менеджере Отменить ревизию Ревизия: Отмена ревизии Изменить комментарий ревизии - Используйте «Shift+Enter» для ввода новой строки. «Enter» - это горячая клавиша кнопки «OK» Запуск. Подождите пожалуйста... СОХРАНИТЬ Сохранить как... @@ -654,16 +696,15 @@ Путь хранения приватного ключа SSH ЗАПУСК Отложить - Автоматически восстанавливать после откладывания - Ваши рабочие файлы остаются неизменными, но отложенные сохранятся. Включить неотслеживаемые файлы - Хранить отложенные файлы Сообщение: Имя тайника (необязательно) - Только подготовленные изменения - Подготовленные так и неподготовленные изменения выбранных файлов будут сохранены!!! + Режим: + Только сформированные изменения + Сформированные так и несформированные изменения выбранных файлов будут сохранены!!! Отложить локальные изменения Принять + Копировать сообщение Отбросить Сохранить как заплатку... Отбросить тайник @@ -682,11 +723,18 @@ ПОДМОДУЛИ Добавить подмодули Копировать относительный путь + Удалить подмодуль Извлечение вложенных подмодулей Открыть подмодуль репозитория Каталог: Относительный путь для хранения подмодуля. Удалить подмодуль + СОСТОЯНИЕ + изменён + не создан + ревизия изменена + не слита + URL-адрес ОК Копировать имя метки Копировать сообщение с метки @@ -700,6 +748,10 @@ Подмодуль: Использовать опцию (--remote) Сетевой адрес: + Журналы + ОЧИСТИТЬ ВСЁ + Копировать + Удалить Предупреждение Приветствие Создать группу @@ -719,17 +771,17 @@ Игнорировать Git Игнорировать все *{0} файлы Игнорировать *{0} файлы в том же каталоге - Игнорировать файлы в том же каталоге + Игнорировать неотслеживаемые файлы в этом каталоге Игнорировать только эти файлы Изменить - Теперь вы можете подготовить этот файл. + Теперь вы можете сформировать этот файл. ЗАФИКСИРОВАТЬ ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории Запустить событие щелчка Зафиксировать (Редактировать) - Подготовить все изменения и зафиксировать - Вы подготовили {0} файл(ов), но отображается только {1} файл(ов) ({2} файл(ов) отфильтровано). Вы хотите продолжить? + Сформировать все изменения и зафиксировать + Вы сформировали {0} файл(ов), но отображается только {1} файл(ов) ({2} файл(ов) отфильтровано). Вы хотите продолжить? ОБНАРУЖЕНЫ КОНФЛИКТЫ ОТКРЫТЬ ВНЕШНИЙ ИНСТРУМЕНТ СЛИЯНИЯ ОТКРЫТЬ ВСЕ КОНФЛИКТЫ ВО ВНЕШНЕМ ИНСТРУМЕНТЕ СЛИЯНИЯ @@ -739,15 +791,16 @@ ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ НЕТ ШАБЛОНОВ РЕВИЗИИ + Сбросить автора Щёлкните правой кнопкой мыши выбранный файл(ы) и разрешите конфликты. Завершение работы - ПОДГОТОВЛЕННЫЕ - СНЯТЬ ПОДГОТОВЛЕННЫЙ - СНЯТЬ ВСЕ ПОДГОТОВЛЕННЫЕ - НЕПОДГОТОВЛЕННЫЕ - ПОДГОТОВИТЬ - ВСЕ ПОДГОТОВИТЬ - ОТКРЫТЬ СПИСОК НЕОТСЛЕЖИВАЕМЫХ ФАЙЛОВ + СФОРМИРОВАННЫЕ + РАСФОРМИРОВАТЬ + РАСФОРМИРОВАТЬ ВСЁ + НЕСФОРМИРОВАННЫЕ + СФОРМИРОВАТЬ + СФОРМИРОВАТЬ ВСЁ + ОТКРЫТЬ СПИСОК НЕОТСЛЕЖИВАЕМЫХ ФАЙЛОВ Шаблон: ${0}$ РАБОЧЕЕ ПРОСТРАНСТВО: Настройка рабочего пространства... diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index b4e4d939..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}$ @@ -241,7 +240,7 @@ நகல் கோப்பு முறை மாற்றப்பட்டது முதல் வேறுபாடு - வெள்ளைவெளி மாற்றத்தை மற்றும் EOL புறக்கணி + வெள்ளைவெளி மாற்றத்தை புறக்கணி கடைசி வேறுபாடு பெகோஅ பொருள் மாற்றம் அடுத்த வேறுபாடு @@ -275,7 +274,6 @@ தேர்ந்தெடுக்கப்பட்ட களஞ்சியத்தைத் திருத்து தனிப்பயன் செயலை இயக்கு செயல் பெயர்: - வேகமாக முன்னோக்கி (சரிபார்க்காமல்) பெறு எல்லா தொலைகளையும் பெறு உள்ளக குறிப்புகளை கட்டாயமாக மீறு @@ -445,6 +443,7 @@ {0} திங்களுக்கு முன்பு {0} ஆண்டுகளுக்கு முன்பு நேற்று + புதிய வரியை உள்ளிட 'உயர்த்து+நுழை' ஐப் பயன்படுத்தவும். 'நுழை' என்பது சரி பொத்தானின் சூடானவிசை ஆகும் விருப்பத்தேர்வுகள் செநு வேறுபாடு உடனடியாக பகுப்பாய்வு செய் @@ -485,7 +484,7 @@ பயனர் மின்னஞ்சல் உலகளாவிய அறிவிலி பயனர் மின்னஞ்சல் --prune எடுக்கும்போது இயக்கு - அறிவிலி (>= 2.23.0) இந்த பயன்பாட்டிற்கு தேவைப்படுகிறது + அறிவிலி (>= 2.25.1) இந்த பயன்பாட்டிற்கு தேவைப்படுகிறது நிறுவல் பாதை உஉபநெ பாகுஅ சரிபார்ப்பை இயக்கு பயனர் பெயர் @@ -509,12 +508,10 @@ `$GIT_COMMON_DIR/பணிமரங்கள்` இதில் பணிமரம் தகவலை கத்தரி இழு தொலை கிளை: - எல்லா கிளைகளையும் எடு இதனுள்: உள்ளக மாற்றங்கள்: நிராகரி பதுக்கிவை & மீண்டும் இடு - குறிச்சொற்கள் இல்லாமல் பெறு தொலை: இழு (எடுத்து ஒன்றிணை) ஒன்றிணை என்பதற்குப் பதிலாக மறுதளத்தைப் பயன்படுத்து @@ -536,7 +533,6 @@ உள்ளக மாற்றங்களை பதுக்கிவை & மீண்டும் இடு மேல்: மறுதளம்: - புதுப்பி தொலையைச் சேர் தொலையைத் திருத்து பெயர்: @@ -565,6 +561,7 @@ தொடர்க தனிப்பயன் செயல்கள் தனிப்பயன் செயல்கள் இல்லை + எல்லா மாற்றங்களையும் நிராகரி '--குறிபதிவு' விருப்பத்தை இயக்கு கோப்பு உலாவியில் திற கிளைகள்/குறிச்சொற்கள்/துணைத் தொகுதிகளைத் தேடு @@ -605,8 +602,7 @@ குறிசொற்கள் புதிய குறிசொல் படைப்பாளர் தேதியின்படி - பெயர் (ஏறுவரிசை) மூலம் - பெயர் (இறகுவரிசை) மூலம் + பெயர் மூலம் வரிசைப்படுத்து முனையத்தில் திற வரலாறுகளில் உறவு நேரத்தைப் பயன்படுத்து @@ -623,7 +619,6 @@ உறுதிமொழி: பின்வாங்கு மாற்றங்களை உறுதிமொழி மாறுசொல் உறுதிமொழி செய்தி - புதிய வரியை உள்ளிட 'உயர்த்து+நுழை' ஐப் பயன்படுத்தவும். 'நுழை' என்பது சரி பொத்தானின் சூடானவிசை ஆகும் இயங்குகிறது. காத்திருக்கவும்... சேமி எனச் சேமி... @@ -649,10 +644,7 @@ தனியார் பாஓடு திறவுகோல் கடை பாதை தொடங்கு பதுக்கிவை - பதுக்கிவைத்த பிறகு தானியங்கி மீட்டமை - உங்கள் செயல்படும் கோப்புகள் மாறாமல் இருக்கும், ஆனால் ஒரு பதுக்கிவைக்கப்படும். கண்காணிக்கப்படாத கோப்புகளைச் சேர் - நிலைப்படுத்தப்பட்ட கோப்புகளை வைத்திரு செய்தி: விருப்பத்தேர்வு. இந்த பதுக்கலின் பெயர் நிலைப்படுத்தப்பட்ட மாற்றங்கள் மட்டும் @@ -713,7 +705,6 @@ அறிவிலி புறக்கணி எல்லா *{0} கோப்புகளையும் புறக்கணி ஒரே கோப்புறையில் *{0} கோப்புகளைப் புறக்கணி - ஒரே கோப்புறையில் கோப்புகளைப் புறக்கணி இந்த கோப்பை மட்டும் புறக்கணி பின்னொட்டு இந்த கோப்பை இப்போது நீங்கள் நிலைப்படுத்தலாம். @@ -737,7 +728,7 @@ நிலைநீக்கு நிலைபடுத்து அனைத்தும் நிலைபடுத்து - மாறாதது எனநினைப்பதை பார் + மாறாதது எனநினைப்பதை பார் வளர்புரு: ${0}$ பணியிடம்: பணியிடங்களை உள்ளமை... diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index a3b63bde..5d120117 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -39,17 +39,17 @@ ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ НЕМАЄ ФАЙЛІВ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ ВИДАЛИТИ + Оновити БІНАРНИЙ ФАЙЛ НЕ ПІДТРИМУЄТЬСЯ!!! Автор рядка ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! Перейти на ${0}$... - Порівняти з HEAD + Порівняти з ${0}$ Порівняти з робочим деревом Копіювати назву гілки Спеціальна дія Видалити ${0}$... Видалити вибрані {0} гілок - Скасувати всі зміни Перемотати до ${0}$ Отримати ${0}$ в ${1}$... Git Flow - Завершити ${0}$ @@ -279,7 +279,6 @@ Редагувати вибраний репозиторій Виконати спеціальну дію Ім'я дії: - Перемотати (без перемкнуття) Витягти Витягти всі віддалені сховища Примусово перезаписати локальні refs @@ -449,6 +448,7 @@ {0} місяців тому {0} років тому Вчора + Використовуйте 'Shift+Enter' для введення нового рядка. 'Enter' - гаряча клавіша кнопки OK Налаштування AI Промпт для аналізу різниці @@ -489,7 +489,7 @@ Email користувача Глобальний email користувача git Увімкнути --prune при fetch - Git (>= 2.23.0) є обов'язковим для цієї програми + Git (>= 2.25.1) є обов'язковим для цієї програми Шлях встановлення Увімкнути перевірку HTTP SSL Ім'я користувача @@ -513,12 +513,10 @@ Видалити застарілу інформацію про робочі дерева в `$GIT_COMMON_DIR/worktrees` Pull (Витягти) Віддалена гілка: - Отримати всі гілки В: Локальні зміни: Скасувати Сховати та Застосувати - Отримати без тегів Віддалене сховище: Pull (Fetch & Merge) Використовувати rebase замість merge @@ -540,7 +538,6 @@ Сховати та застосувати локальні зміни На: Перебазувати: - Оновити Додати віддалене сховище Редагувати віддалене сховище Назва: @@ -569,6 +566,7 @@ ПРОДОВЖИТИ Спеціальні дії Немає спеціальних дій + Скасувати всі зміни Увімкнути опцію '--reflog' Відкрити у файловому менеджері Пошук гілок/тегів/підмодулів @@ -609,8 +607,7 @@ ТЕГИ НОВИЙ ТЕГ За датою створення - За назвою (за зростанням) - За назвою (за спаданням) + За назвою Сортувати Відкрити в терміналі Використовувати відносний час в історії @@ -627,7 +624,6 @@ Коміт: Закомітити зміни скасування Змінити повідомлення коміту - Використовуйте 'Shift+Enter' для введення нового рядка. 'Enter' - гаряча клавіша кнопки OK Виконується. Будь ласка, зачекайте... ЗБЕРЕГТИ Зберегти як... @@ -653,10 +649,7 @@ Шлях до сховища приватного ключа SSH ПОЧАТИ Stash (Сховати) - Автоматично відновити після схову - Ваші робочі файли залишаться без змін, але буде збережено схованку. Включити невідстежувані файли - Зберегти проіндексовані файли Повідомлення: Необов'язково. Назва цієї схованки Лише проіндексовані зміни @@ -718,7 +711,6 @@ Git Ignore Ігнорувати всі файли *{0} Ігнорувати файли *{0} у цій же теці - Ігнорувати файли у цій же теці Ігнорувати лише цей файл Amend (Доповнити) Тепер ви можете проіндексувати цей файл. @@ -746,7 +738,7 @@ НЕПРОІНДЕКСОВАНІ ІНДЕКСУВАТИ ІНДЕКСУВАТИ ВСЕ - ПЕРЕГЛЯНУТИ ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + ПЕРЕГЛЯНУТИ ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ Шаблон: ${0}$ РОБОЧИЙ ПРОСТІР: Налаштувати робочі простори... diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3d41bccd..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 @@ 未提交更改 : 丢弃更改 贮藏并自动恢复 + 同时更新所有子模块 目标分支 : + 检出分支并快进 + 上游分支 : 挑选提交 提交信息中追加来源信息 提交列表 : @@ -112,6 +124,7 @@ 交互式变基(rebase -i) ${0}$ 到此处 合并(merge)此提交至 ${0}$ 合并(merge)... + 推送(push) ${0}$ 到 ${1}$ 变基(rebase) ${0}$ 到此处 重置(reset) ${0}$ 到此处 回滚此提交 @@ -120,6 +133,7 @@ 合并此提交到上一个提交 合并之后的提交到此处 变更对比 + 个文件发生变更 查找变更... 文件列表 LFS文件 @@ -211,6 +225,7 @@ 填写分支名称。 空格将被替换为'-'符号 创建本地分支 + 允许重置已存在的分支 新建标签 ... 标签位于 : 使用GPG签名 @@ -225,6 +240,9 @@ 轻量标签 按住Ctrl键点击将以默认参数运行 剪切 + 取消初始化子模块 + 强制取消,即使包含本地变更 + 子模块 : 删除分支确认 分支名 : 您正在删除远程上的分支,请务必小心!!! @@ -250,7 +268,7 @@ 复制 文件权限已变化 首个差异 - 忽略空白符号变化和EOL + 忽略空白符号变化 最后一个差异 LFS对象变更 下一个差异 @@ -260,6 +278,7 @@ 显示隐藏符号 分列对比 子模块 + 删除 新增 交换比对双方 语法高亮 @@ -284,7 +303,6 @@ 编辑仓库 执行自定义操作 自定义操作 : - 快进(fast-forward,无需checkout) 拉取(fetch) 拉取所有的远程仓库 强制覆盖本地REFs @@ -319,6 +337,8 @@ 结束修复分支 结束版本分支 目标分支 : + 完成后自动推送 + 压缩变更为单一提交后合并分支 修复分支 : 修复分支名前缀 : 初始化GIT工作流 @@ -380,6 +400,8 @@ 切换到上一个页面 新建页面 打开偏好设置面板 + 切换工作区 + 切换显示页面 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 @@ -400,6 +422,7 @@ 关闭搜索 定位到下一个匹配搜索的位置 定位到上一个匹配搜索的位置 + 使用外部比对工具查看 打开搜索 丢弃 暂存 @@ -421,7 +444,10 @@ 在浏览器中访问 出错了 系统提示 + 工作区列表 + 页面列表 合并分支 + 编辑合并信息 目标分支 : 合并方式 : 合并目标 : @@ -454,6 +480,7 @@ {0}个月前 {0}年前 昨天 + 请使用Shift+Enter换行。Enter键已被【确 定】按钮占用。 偏好设置 AI Analyze Diff Prompt @@ -494,7 +521,8 @@ 邮箱 默认GIT用户邮箱 拉取更新时启用修剪(--prune) - 本软件要求GIT最低版本为2.23.0 + 对比文件时,默认忽略换行符变更 (--ignore-cr-at-eol) + 本软件要求GIT最低版本为2.25.1 安装路径 启用HTTP SSL验证 用户名 @@ -518,12 +546,11 @@ 清理在`$GIT_COMMON_DIR/worktrees`中的无效工作树信息 拉回(pull) 拉取分支 : - 拉取远程中的所有分支变更 本地分支 : 未提交更改 : 丢弃更改 贮藏并自动恢复 - 不拉取远程标签 + 同时更新所有子模块 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -532,6 +559,8 @@ 启用强制推送 本地分支 : 远程仓库 : + 修订 : + 推送指定修订到远程仓库 推送到远程仓库 远程分支 : 跟踪远程分支 @@ -545,7 +574,6 @@ 自动贮藏并恢复本地变更 目标提交 : 分支 : - 重新加载 添加远程仓库 编辑远程仓库 远程名 : @@ -567,13 +595,18 @@ 分支 : 终止合并 自动拉取远端变更中... + 排序方式 + 按提交时间 + 按名称 清理本仓库(GC) 本操作将执行`git gc`命令。 清空过滤规则 + 清空 配置本仓库 下一步 自定义操作 自定义操作未设置 + 放弃所有更改 启用 --reflog 选项 在文件浏览器中打开 快速查找分支/标签/子模块 @@ -601,10 +634,12 @@ 查找提交 作者 提交者 + 变更内容 文件 提交信息 提交指纹 仅在当前分支中查找 + 以树型结构展示 以树型结构展示 跳过此提交 提交统计 @@ -614,12 +649,12 @@ 标签列表 新建标签 按创建时间 - 按名称(升序) - 按名称(降序) + 按名称 排序 在终端中打开 在提交列表中使用相对时间 查看命令日志 + 访问远程仓库 '{0}' 工作树列表 新增工作树 清理 @@ -628,12 +663,14 @@ 重置模式 : 提交 : 当前分支 : + 重置所选分支(非当前分支) + 重置点 : + 操作分支 : 在文件浏览器中查看 回滚操作确认 目标提交 : 回滚后提交更改 编辑提交信息 - 请使用Shift+Enter换行。Enter键已被【确 定】按钮占用。 执行操作中,请耐心等待... 保 存 另存为... @@ -659,16 +696,15 @@ SSH密钥文件 开 始 贮藏(stash) - 贮藏后自动恢复工作区 - 工作区文件保持未修改状态,但贮藏内容已保存。 包含未跟踪的文件 - 保留暂存区文件 信息 : - 选填,用于命名此贮藏 + 选填,此贮藏的描述信息 + 模式 : 仅贮藏暂存区的变更 选中文件的所有变更均会被贮藏! 贮藏本地变更 应用(apply) + 复制描述信息 删除(drop) 另存为补丁... 丢弃贮藏确认 @@ -687,11 +723,18 @@ 子模块 添加子模块 复制路径 + 取消初始化 拉取子孙模块 打开仓库 相对仓库路径 : 本地存放的相对路径。 删除子模块 + 状态 + 未提交修改 + 未初始化 + SHA变更 + 未解决冲突 + 仓库 确 定 复制标签名 复制标签信息 @@ -728,7 +771,7 @@ 添加至 .gitignore 忽略列表 忽略所有 *{0} 文件 忽略同目录下所有 *{0} 文件 - 忽略同目录下所有文件 + 忽略该目录下的新文件 忽略本文件 修补 现在您已可将其加入暂存区中 @@ -748,6 +791,7 @@ 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 + 重置提交者 请选中冲突文件,打开右键菜单,选择合适的解决方式 署名 已暂存 @@ -756,7 +800,7 @@ 未暂存 暂存选中 暂存所有 - 查看忽略变更文件 + 查看忽略变更文件 模板:${0}$ 工作区: 配置工作区... diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 3a10f6ca..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 @@ 未提交變更: 捨棄變更 擱置變更並自動復原 + 同時更新所有子模組 目標分支: + 簽出分支並快轉 + 上游分支 : 揀選提交 提交資訊中追加來源資訊 提交列表: @@ -112,6 +124,7 @@ 互動式重定基底 (rebase -i) ${0}$ 到此處 合併 (merge) 此提交到 ${0}$ 合併 (merge)... + 推送(push) ${0}$ 至 ${1}$ 重定基底 (rebase) ${0}$ 到此處 重設 (reset) ${0}$ 到此處 復原此提交 @@ -120,6 +133,7 @@ 合併此提交到上一個提交 合併之後的提交到此處 變更對比 + 個檔案已變更 搜尋變更... 檔案列表 LFS 檔案 @@ -161,7 +175,7 @@ 啟用定時自動提取 (fetch) 遠端更新 分鐘 預設遠端存放庫 - 首選合併模式 + 預設合併模式 Issue 追蹤 新增符合 Azure DevOps 規則 新增符合 Gitee 議題規則 @@ -186,10 +200,10 @@ 顏色 名稱 啟動時還原上次開啟的存放庫 - 确认继续 + 確認繼續 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)? - 自动暂存并提交 - 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)或者自動暫存全部變更並提交? + 自動暫存並提交 + 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交? 產生約定式提交訊息 破壞性變更: 關閉的 Issue: @@ -211,6 +225,7 @@ 輸入分支名稱。 空格將以英文破折號取代 建立本機分支 + 允許覆寫現有分支 新增標籤... 標籤位於: 使用 GPG 簽章 @@ -225,6 +240,9 @@ 輕量標籤 按住 Ctrl 鍵將直接以預設參數執行 剪下 + 取消初始化子模組 + 強制取消,即使它包含本地變更 + 子模組 : 刪除分支確認 分支名稱: 您正在刪除遠端上的分支,請務必小心! @@ -250,7 +268,7 @@ 複製 檔案權限已變更 第一個差異 - 忽略空白符號變化和EOL + 忽略空白符號變化 最後一個差異 LFS 物件變更 下一個差異 @@ -260,6 +278,7 @@ 顯示隱藏符號 並排對比 子模組 + 已刪除 新增 交換比對雙方 語法上色 @@ -284,7 +303,6 @@ 編輯存放庫 執行自訂動作 自訂動作: - 快進 (fast-forward,無需 checkout) 提取 (fetch) 提取所有的遠端存放庫 強制覆寫本機 REFs @@ -306,8 +324,8 @@ 取消暫存 從暫存中移除 {0} 個檔案 取消暫存選取的變更 - 使用我方版本 (checkout --ours) - 使用對方版本 (checkout --theirs) + 使用我方版本 (ours) + 使用對方版本 (theirs) 檔案歷史 檔案變更 檔案内容 @@ -319,6 +337,8 @@ 完成修復分支 完成發行分支 目標分支: + 完成後自動推送 + 壓縮為單一提交後合併 修復分支: 修復分支前置詞: 初始化 Git 工作流 @@ -380,6 +400,8 @@ 切換到上一個頁面 新增頁面 開啟偏好設定面板 + 切換工作區 + 切換目前頁面 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 @@ -400,6 +422,7 @@ 關閉搜尋面板 前往下一個搜尋相符的位置 前往上一個搜尋相符的位置 + 使用外部比對工具檢視 開啟搜尋面板 捨棄 暫存 @@ -421,7 +444,10 @@ 在瀏覽器中開啟連結 發生錯誤 系統提示 + 工作區列表 + 頁面列表 合併分支 + 編輯合併訊息 目標分支: 合併方式: 合併來源: @@ -454,6 +480,7 @@ {0} 個月前 {0} 年前 昨天 + 請使用 Shift + Enter 換行。Enter 鍵已被 [確定] 按鈕佔用。 偏好設定 AI 分析變更差異提示詞 @@ -465,7 +492,7 @@ 啟用串流輸出 外觀設定 預設字型 - 編輯器制表符寬度 + 編輯器 Tab 寬度 字型大小 預設 程式碼 @@ -494,7 +521,8 @@ 電子郵件 預設 Git 使用者電子郵件 拉取變更時進行清理 (--prune) - 本軟體要求 Git 最低版本為 2.23.0 + 對比檔案時,預設忽略行尾的 CR 變更 (--ignore-cr-at-eol) + 本軟體要求 Git 最低版本為 2.25.1 安裝路徑 啟用 HTTP SSL 驗證 使用者名稱 @@ -518,12 +546,11 @@ 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作區資訊 拉取 (pull) 拉取分支: - 拉取遠端中的所有分支 本機分支: 未提交變更: 捨棄變更 擱置變更並自動復原 - 不拉取遠端標籤 + 同時更新所有子模組 遠端: 拉取 (提取並合併) 使用重定基底 (rebase) 合併分支 @@ -532,6 +559,8 @@ 啟用強制推送 本機分支: 遠端存放庫: + 修訂: + 推送修訂到遠端存放庫 推送到遠端存放庫 遠端分支: 追蹤遠端分支 @@ -545,7 +574,6 @@ 自動擱置變更並復原本機變更 目標提交: 分支: - 重新載入 新增遠端存放庫 編輯遠端存放庫 遠端名稱: @@ -567,13 +595,18 @@ 分支: 中止 自動提取遠端變更中... + 排序 + 依建立時間 + 依名稱升序 清理本存放庫 (GC) 本操作將執行 `git gc` 命令。 清空篩選規則 + 清空 設定本存放庫 下一步 自訂動作 沒有自訂的動作 + 捨棄所有變更 啟用 [--reflog] 選項 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 @@ -601,10 +634,12 @@ 搜尋提交 作者 提交者 + 變更內容 檔案 提交訊息 提交編號 僅搜尋目前分支 + 以樹型結構展示 以樹型結構展示 跳過此提交 提交統計 @@ -614,12 +649,12 @@ 標籤列表 新增標籤 依建立時間 - 依名稱升序 - 依名稱降序 + 依名稱 排序 在終端機中開啟 在提交列表中使用相對時間 - 檢視 GIT 指令的日誌 + 檢視 Git 指令記錄 + 檢視遠端存放庫 '{0}' 工作區列表 新增工作區 清理 @@ -628,12 +663,14 @@ 重設模式: 移至提交: 目前分支: + 重設選取的分支(非目前分支) + 重設位置 : + 選取分支 : 在檔案瀏覽器中檢視 復原操作確認 目標提交: 復原後提交變更 編輯提交訊息 - 請使用 Shift + Enter 換行。Enter 鍵已被 [確定] 按鈕佔用。 執行操作中,請耐心等待... 儲存 另存新檔... @@ -659,16 +696,15 @@ SSH 金鑰檔案 開 始 擱置變更 (stash) - 擱置變更後自動復原工作區 - 工作區檔案保持未修改,但擱置內容已儲存。 包含未追蹤的檔案 - 保留已暫存的變更 擱置變更訊息: - 選填,用於命名此擱置變更 + 選填,用於描述此擱置變更 + 操作模式: 僅擱置已暫存的變更 已選取的檔案中的變更均會被擱置! 擱置本機變更 套用 (apply) + 複製描述訊息 刪除 (drop) 另存為修補檔 (patch)... 捨棄擱置變更確認 @@ -687,11 +723,18 @@ 子模組 新增子模組 複製路徑 + 取消初始化 提取子模組 開啟存放庫 相對存放庫路徑: 本機存放的相對路徑。 刪除子模組 + 狀態 + 未提交變更 + 未初始化 + SHA 變更 + 未解決的衝突 + 存放庫 確 定 複製標籤名稱 複製標籤訊息 @@ -705,8 +748,8 @@ 子模組: 啟用 [--remote] 選項 存放庫網址: - 日誌清單 - 清除所有日誌 + 記錄 + 清除所有記錄 複製 刪除 警告 @@ -728,7 +771,7 @@ 加入至 .gitignore 忽略清單 忽略所有 *{0} 檔案 忽略同路徑下所有 *{0} 檔案 - 忽略同路徑下所有檔案 + 忽略本路徑下的新增檔案 忽略本檔案 修補 現在您已可將其加入暫存區中 @@ -738,16 +781,17 @@ 觸發點擊事件 提交 (修改原始提交) 自動暫存全部變更並提交 - 您已暫存 {0} 檔案,但只顯示 {1} 檔案 ({2} 檔案被篩選器隱藏)。您要繼續嗎? - 檢測到衝突 + 您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎? + 偵測到衝突 使用外部合併工具開啟 使用外部合併工具開啟 檔案衝突已解決 - 使用 MINE - 使用 THEIRS + 使用我方版本 (ours) + 使用對方版本 (theirs) 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 + 重設作者 請選擇發生衝突的檔案,開啟右鍵選單,選擇合適的解決方式 署名 已暫存 @@ -756,7 +800,7 @@ 未暫存 暫存選取的檔案 暫存所有檔案 - 檢視不追蹤變更的檔案 + 檢視不追蹤變更的檔案 範本: ${0}$ 工作區: 設定工作區... diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 093ae4e0..f42160a2 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -16,7 +16,7 @@ 12 - + @@ -38,9 +38,9 @@ - + - + - + - + - + @@ -737,7 +731,7 @@ - + @@ -949,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 a7ff5015..49f245dd 100644 --- a/src/Views/NameHighlightedTextBlock.cs +++ b/src/Views/NameHighlightedTextBlock.cs @@ -55,9 +55,10 @@ namespace SourceGit.Views 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, @@ -94,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 86026bed..6b26f476 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -129,7 +129,7 @@ - + + - + + @@ -202,9 +217,26 @@ - + - + + + + + + - + - - + @@ -237,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 577849df..163ce031 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -136,7 +136,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 ad2f8cea..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 = @@ -765,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(); @@ -1047,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; } @@ -1061,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; } @@ -1243,15 +1242,14 @@ 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)"); } else @@ -1402,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) @@ -1494,7 +1491,7 @@ 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)"); } else @@ -1529,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; } @@ -1740,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; }); @@ -1860,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 { @@ -1921,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 { diff --git a/src/Views/ViewLogs.axaml b/src/Views/ViewLogs.axaml index e29a5afd..a3b9e240 100644 --- a/src/Views/ViewLogs.axaml +++ b/src/Views/ViewLogs.axaml @@ -12,7 +12,7 @@ Title="{DynamicResource Text.ViewLogs}" Icon="/App.ico" Width="800" Height="500" - CanResize="False" + CanResize="True" WindowStartupLocation="CenterOwner"> @@ -38,7 +38,13 @@ - + + + + + + + @@ -71,31 +78,44 @@ - + - + - - + + + + + + - + @@ -61,8 +61,7 @@ ItemsSource="{Binding Rows}" SelectionMode="Single" Loaded="SetupTreeViewDragAndDrop" - LostFocus="OnTreeViewLostFocus" - KeyDown="OnTreeViewKeyDown"> + LostFocus="OnTreeViewLostFocus"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/WorkspaceSwitcher.axaml.cs b/src/Views/WorkspaceSwitcher.axaml.cs new file mode 100644 index 00000000..87743d9f --- /dev/null +++ b/src/Views/WorkspaceSwitcher.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class WorkspaceSwitcher : UserControl + { + public WorkspaceSwitcher() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Enter && DataContext is ViewModels.WorkspaceSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.WorkspaceSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && WorkspaceListBox.ItemCount > 0) + { + WorkspaceListBox.Focus(NavigationMethod.Directional); + + if (WorkspaceListBox.SelectedIndex < 0) + WorkspaceListBox.SelectedIndex = 0; + else if (WorkspaceListBox.SelectedIndex < WorkspaceListBox.ItemCount) + WorkspaceListBox.SelectedIndex++; + + e.Handled = true; + } + } + } +}