Compare commits

..

No commits in common. "master" and "v2025.14" have entirely different histories.

311 changed files with 4436 additions and 10016 deletions

2
.gitignore vendored
View file

@ -37,5 +37,3 @@ build/*.deb
build/*.rpm build/*.rpm
build/*.AppImage build/*.AppImage
SourceGit.app/ SourceGit.app/
build.command
src/Properties/launchSettings.json

View file

@ -39,7 +39,6 @@
* Search commits * Search commits
* GitFlow * GitFlow
* Git LFS * Git LFS
* Bisect
* Issue Link * Issue Link
* Workspace * Workspace
* Custom Action * Custom Action
@ -54,9 +53,9 @@ You can find the current translation status in [TRANSLATION.md](https://github.c
## How to Use ## How to Use
**To use this tool, you need to install Git(>=2.25.1) first.** **To use this tool, you need to install Git(>=2.23.0) 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. This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
@ -93,7 +92,7 @@ For **macOS** users:
brew tap ybeapps/homebrew-sourcegit brew tap ybeapps/homebrew-sourcegit
brew install --cask --no-quarantine 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 ```shell
sudo xattr -cr /Applications/SourceGit.app sudo xattr -cr /Applications/SourceGit.app
``` ```

View file

@ -6,220 +6,145 @@ 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) ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)
### ![de__DE](https://img.shields.io/badge/de__DE-%E2%88%9A-brightgreen) ### ![de__DE](https://img.shields.io/badge/de__DE-96.19%25-yellow)
### ![es__ES](https://img.shields.io/badge/es__ES-99.13%25-yellow) <details>
<summary>Missing keys in de_DE.axaml</summary>
- 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
</details>
### ![es__ES](https://img.shields.io/badge/es__ES-98.95%25-yellow)
<details> <details>
<summary>Missing keys in es_ES.axaml</summary> <summary>Missing keys in es_ES.axaml</summary>
- Text.CommitCM.PushRevision - Text.CommitCM.CopyAuthor
- Text.Merge.Edit - Text.CommitCM.CopyCommitter
- Text.Push.Revision - Text.CommitCM.CopySubject
- Text.Push.Revision.Title - Text.Repository.ViewLogs
- Text.Stash.Mode - Text.ViewLogs
- Text.StashCM.CopyMessage - Text.ViewLogs.Clear
- Text.WorkingCopy.AddToGitIgnore.InFolder - Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
</details> </details>
### ![fr__FR](https://img.shields.io/badge/fr__FR-91.19%25-yellow) ### ![fr__FR](https://img.shields.io/badge/fr__FR-97.51%25-yellow)
<details> <details>
<summary>Missing keys in fr_FR.axaml</summary> <summary>Missing keys in fr_FR.axaml</summary>
- 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.CopyAuthor
- Text.CommitCM.CopyCommitter - Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject - Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount - Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode - Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges - 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.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
- Text.ViewLogs.Clear - Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog - Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete - Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs - Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details> </details>
### ![it__IT](https://img.shields.io/badge/it__IT-96.53%25-yellow) ### ![it__IT](https://img.shields.io/badge/it__IT-97.24%25-yellow)
<details> <details>
<summary>Missing keys in it_IT.axaml</summary> <summary>Missing keys in it_IT.axaml</summary>
- 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
</details>
### ![ja__JP](https://img.shields.io/badge/ja__JP-90.94%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- 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.CopyAuthor
- Text.CommitCM.CopyCommitter - Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject - Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount - Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode - Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges - Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting - Text.CopyFullPath
- Text.DeinitSubmodule - Text.Preferences.General.ShowTagsInGraph
- 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.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs - 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
- Text.ViewLogs.Clear - Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog - Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete - Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs - Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details> </details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-83.25%25-yellow) ### ![ja__JP](https://img.shields.io/badge/ja__JP-97.24%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- 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.Repository.FilterCommits
- Text.Repository.Tags.OrderByNameDes
- 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
</details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-88.71%25-yellow)
<details> <details>
<summary>Missing keys in pt_BR.axaml</summary> <summary>Missing keys in pt_BR.axaml</summary>
@ -230,29 +155,15 @@ This document shows the translation status of each locale file in the repository
- Text.ApplyStash.DropAfterApply - Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex - Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash - 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.CustomAction
- Text.BranchCM.MergeMultiBranches - Text.BranchCM.MergeMultiBranches
- Text.BranchCM.ResetToSelectedCommit
- Text.BranchUpstreamInvalid - Text.BranchUpstreamInvalid
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.Clone.RecurseSubmodules - Text.Clone.RecurseSubmodules
- Text.CommitCM.CopyAuthor - Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter - Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject - Text.CommitCM.CopySubject
- Text.CommitCM.Merge - Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple - Text.CommitCM.MergeMultiple
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitDetail.Files.Search - Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children - Text.CommitDetail.Info.Children
- Text.CommitMessageTextBox.SubjectCount - Text.CommitMessageTextBox.SubjectCount
@ -267,32 +178,19 @@ This document shows the translation status of each locale file in the repository
- Text.ConfirmEmptyCommit.WithLocalChanges - Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath - Text.CopyFullPath
- Text.CreateBranch.Name.WarnSpace - Text.CreateBranch.Name.WarnSpace
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.DeleteRepositoryNode.Path - Text.DeleteRepositoryNode.Path
- Text.DeleteRepositoryNode.TipForGroup - Text.DeleteRepositoryNode.TipForGroup
- Text.DeleteRepositoryNode.TipForRepository - Text.DeleteRepositoryNode.TipForRepository
- Text.Diff.First - Text.Diff.First
- Text.Diff.Last - Text.Diff.Last
- Text.Diff.Submodule.Deleted
- Text.Diff.UseBlockNavigation - Text.Diff.UseBlockNavigation
- Text.Fetch.Force - Text.Fetch.Force
- Text.FileCM.ResolveUsing - Text.FileCM.ResolveUsing
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.Clone - Text.Hotkeys.Global.Clone
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.InProgress.CherryPick.Head - Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating - Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt - Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head - Text.InProgress.Revert.Head
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Merge.Source - Text.Merge.Source
- Text.MergeMultiple - Text.MergeMultiple
- Text.MergeMultiple.CommitChanges - Text.MergeMultiple.CommitChanges
@ -303,15 +201,7 @@ This document shows the translation status of each locale file in the repository
- Text.Preferences.General.DateFormat - Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren - Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph - Text.Preferences.General.ShowTagsInGraph
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Preferences.Git.SSLVerify - 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.FilterCommits
- Text.Repository.HistoriesLayout - Text.Repository.HistoriesLayout
- Text.Repository.HistoriesLayout.Horizontal - Text.Repository.HistoriesLayout.Horizontal
@ -319,198 +209,94 @@ This document shows the translation status of each locale file in the repository
- Text.Repository.HistoriesOrder - Text.Repository.HistoriesOrder
- Text.Repository.Notifications.Clear - Text.Repository.Notifications.Clear
- Text.Repository.OnlyHighlightCurrentBranchInHistories - Text.Repository.OnlyHighlightCurrentBranchInHistories
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.Skip - Text.Repository.Skip
- Text.Repository.Tags.OrderByCreatorDate - Text.Repository.Tags.OrderByCreatorDate
- Text.Repository.Tags.OrderByName - Text.Repository.Tags.OrderByNameAsc
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.Tags.Sort - Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories - Text.Repository.UseRelativeTimeInHistories
- Text.Repository.ViewLogs - Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.SetUpstream - Text.SetUpstream
- Text.SetUpstream.Local - Text.SetUpstream.Local
- Text.SetUpstream.Unset - Text.SetUpstream.Unset
- Text.SetUpstream.Upstream - Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo - Text.SHALinkCM.NavigateTo
- Text.Stash.Mode - Text.Stash.AutoRestore
- Text.StashCM.CopyMessage - Text.Stash.AutoRestore.Tip
- Text.StashCM.SaveAsPatch - 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
- Text.ViewLogs.Clear - Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog - Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete - Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.CommitToEdit - Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs - Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
- Text.WorkingCopy.SignOff - Text.WorkingCopy.SignOff
</details> </details>
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ![ru__RU](https://img.shields.io/badge/ru__RU-98.82%25-yellow)
### ![ta__IN](https://img.shields.io/badge/ta__IN-91.07%25-yellow) <details>
<summary>Missing keys in ru_RU.axaml</summary>
- 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
</details>
### ![ta__IN](https://img.shields.io/badge/ta__IN-97.51%25-yellow)
<details> <details>
<summary>Missing keys in ta_IN.axaml</summary> <summary>Missing keys in ta_IN.axaml</summary>
- 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.CopyAuthor
- Text.CommitCM.CopyCommitter - Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject - Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount - Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode - Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges - 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.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.UpdateSubmodules.Target
- Text.ViewLogs - Text.ViewLogs
- Text.ViewLogs.Clear - Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog - Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete - Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs - Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details> </details>
### ![uk__UA](https://img.shields.io/badge/uk__UA-92.31%25-yellow) ### ![uk__UA](https://img.shields.io/badge/uk__UA-98.69%25-yellow)
<details> <details>
<summary>Missing keys in uk_UA.axaml</summary> <summary>Missing keys in uk_UA.axaml</summary>
- 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.CopyAuthor
- Text.CommitCM.CopyCommitter - Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject - Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount - Text.CommitMessageTextBox.SubjectCount
- Text.ConfigureWorkspace.Name - 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.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
- Text.ViewLogs.Clear - Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog - Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete - Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ResetAuthor
</details> </details>

View file

@ -1 +1 @@
2025.23 2025.14

View file

@ -12,4 +12,4 @@
dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj
``` ```
> [!NOTE] > [!NOTE]
> 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. > 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.

View file

@ -37,10 +37,10 @@ namespace SourceGit
} }
} }
public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false)); public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false)); public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir)); public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false)); public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true)); 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 QuitCommand = new Command(_ => Quit(0));
public static readonly Command CopyTextBlockCommand = new Command(p => public static readonly Command CopyTextBlockCommand = new Command(p =>

View file

@ -35,7 +35,7 @@
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/> <NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" Gesture="F1"/> <NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/> <NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/> <NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/> <NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>

View file

@ -78,7 +78,7 @@ namespace SourceGit
return builder; return builder;
} }
public static void LogException(Exception ex) private static void LogException(Exception ex)
{ {
if (ex == null) if (ex == null)
return; return;
@ -105,44 +105,10 @@ namespace SourceGit
#endregion #endregion
#region Utility Functions #region Utility Functions
public static void ShowWindow(object data, bool showAsDialog) public static void OpenDialog(Window window)
{
var impl = (Views.ChromelessWindow target, bool isDialog) =>
{ {
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{ window.ShowDialog(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) public static void RaiseException(string context, string message)
@ -301,7 +267,7 @@ namespace SourceGit
return await clipboard.GetTextAsync(); return await clipboard.GetTextAsync();
} }
} }
return null; return default;
} }
public static string Text(string key, params object[] args) public static string Text(string key, params object[] args)
@ -323,7 +289,8 @@ namespace SourceGit
icon.Height = 12; icon.Height = 12;
icon.Stretch = Stretch.Uniform; icon.Stretch = Stretch.Uniform;
if (Current?.FindResource(key) is StreamGeometry geo) var geo = Current?.FindResource(key) as StreamGeometry;
if (geo != null)
icon.Data = geo; icon.Data = geo;
return icon; return icon;
@ -337,7 +304,7 @@ namespace SourceGit
return null; return null;
} }
public static ViewModels.Launcher GetLauncher() public static ViewModels.Launcher GetLauncer()
{ {
return Current is App app ? app._launcher : null; return Current is App app ? app._launcher : null;
} }
@ -375,14 +342,6 @@ namespace SourceGit
{ {
BindingPlugins.DataValidators.RemoveAt(0); BindingPlugins.DataValidators.RemoveAt(0);
// Disable tooltip if window is not active.
ToolTip.ToolTipOpeningEvent.AddClassHandler<Control>((c, e) =>
{
var topLevel = TopLevel.GetTopLevel(c);
if (topLevel is not Window { IsActive: true })
e.Cancel = true;
});
if (TryLaunchAsCoreEditor(desktop)) if (TryLaunchAsCoreEditor(desktop))
return; return;
@ -392,17 +351,7 @@ namespace SourceGit
_ipcChannel = new Models.IpcChannel(); _ipcChannel = new Models.IpcChannel();
if (!_ipcChannel.IsFirstInstance) if (!_ipcChannel.IsFirstInstance)
{ {
var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty; _ipcChannel.SendToFirstInstance(desktop.Args is { Length: 1 } ? desktop.Args[0] : 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); Environment.Exit(0);
} }
else else
@ -496,7 +445,7 @@ namespace SourceGit
if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead))
return true; 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) if (done.Length == 0)
return true; return true;
@ -521,7 +470,7 @@ namespace SourceGit
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{ {
var args = desktop.Args; var args = desktop.Args;
if (args is not { Length: > 1 } || !args[0].Equals("--core-editor", StringComparison.Ordinal)) if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
return false; return false;
var file = args[1]; var file = args[1];
@ -531,8 +480,8 @@ namespace SourceGit
return true; return true;
} }
var editor = new Views.CommitMessageEditor(); var editor = new Views.StandaloneCommitMessageEditor();
editor.AsStandalone(file); editor.SetFile(file);
desktop.MainWindow = editor; desktop.MainWindow = editor;
return true; return true;
} }
@ -557,7 +506,7 @@ namespace SourceGit
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{ {
Native.OS.SetupExternalTools(); Native.OS.SetupEnternalTools();
Models.AvatarManager.Instance.Start(); Models.AvatarManager.Instance.Start();
string startupRepo = null; string startupRepo = null;
@ -569,7 +518,6 @@ namespace SourceGit
_launcher = new ViewModels.Launcher(startupRepo); _launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
#if !DISABLE_UPDATE_DETECTION #if !DISABLE_UPDATE_DETECTION
if (pref.ShouldCheck4UpdateOnStartup()) if (pref.ShouldCheck4UpdateOnStartup())
@ -650,7 +598,11 @@ namespace SourceGit
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } };
dialog.ShowDialog(owner);
}
}); });
} }
@ -680,15 +632,7 @@ namespace SourceGit
prevChar = c; prevChar = c;
} }
var name = sb.ToString(); trimmed.Add(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; return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only. <!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls. Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="SourceGit.Desktop"/> <assemblyIdentity version="1.0.0.0" name="SourceGit.Desktop"/>

View file

@ -1,4 +1,7 @@
namespace SourceGit.Commands using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{ {
public class Add : Command public class Add : Command
{ {
@ -9,11 +12,20 @@
Args = includeUntracked ? "add ." : "add -u ."; Args = includeUntracked ? "add ." : "add -u .";
} }
public Add(string repo, Models.Change change) public Add(string repo, List<string> changes)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"add -- \"{change.Path}\"";
var builder = new StringBuilder();
builder.Append("add --");
foreach (var c in changes)
{
builder.Append(" \"");
builder.Append(c);
builder.Append("\"");
}
Args = builder.ToString();
} }
public Add(string repo, string pathspecFromFile) public Add(string repo, string pathspecFromFile)

View file

@ -1,13 +0,0 @@
namespace SourceGit.Commands
{
public class Bisect : Command
{
public Bisect(string repo, string subcmd)
{
WorkingDirectory = repo;
Context = repo;
RaiseError = false;
Args = $"bisect {subcmd}";
}
}
}

View file

@ -51,7 +51,7 @@ namespace SourceGit.Commands
private void ParseLine(string line) private void ParseLine(string line)
{ {
if (line.Contains('\0', StringComparison.Ordinal)) if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
{ {
_result.IsBinary = true; _result.IsBinary = true;
_result.LineInfos.Clear(); _result.LineInfos.Clear();
@ -89,7 +89,7 @@ namespace SourceGit.Commands
private readonly Models.BlameData _result = new Models.BlameData(); private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder(); private readonly StringBuilder _content = new StringBuilder();
private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly; private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
private string _lastSHA = string.Empty; private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false; private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64; private int _minSHALen = 64;

View file

@ -1,6 +1,4 @@
using System.Text; namespace SourceGit.Commands
namespace SourceGit.Commands
{ {
public static class Branch public static class Branch
{ {
@ -13,20 +11,12 @@ namespace SourceGit.Commands
return cmd.ReadToEnd().StdOut.Trim(); return cmd.ReadToEnd().StdOut.Trim();
} }
public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log) public static bool Create(string repo, string name, string basedOn, 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(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = builder.ToString(); cmd.Args = $"branch {name} {basedOn}";
cmd.Log = log; cmd.Log = log;
return cmd.Exec(); return cmd.Exec();
} }

View file

@ -11,37 +11,15 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
} }
public bool Branch(string branch, bool force) public bool Branch(string branch)
{ {
var builder = new StringBuilder(); Args = $"checkout --recurse-submodules --progress {branch}";
builder.Append("checkout --progress ");
if (force)
builder.Append("--force ");
builder.Append(branch);
Args = builder.ToString();
return Exec(); return Exec();
} }
public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite) public bool Branch(string branch, string basedOn)
{ {
var builder = new StringBuilder(); Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}";
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(); return Exec();
} }
@ -78,5 +56,11 @@ namespace SourceGit.Commands
Args = $"checkout --no-overlay {revision} -- \"{file}\""; Args = $"checkout --no-overlay {revision} -- \"{file}\"";
return Exec(); return Exec();
} }
public bool Commit(string commitId)
{
Args = $"checkout --detach --progress {commitId}";
return Exec();
}
} }
} }

View file

@ -1,12 +1,31 @@
namespace SourceGit.Commands using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{ {
public class Clean : Command public class Clean : Command
{ {
public Clean(string repo) public Clean(string repo, bool includeIgnored)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "clean -qfdx"; Args = includeIgnored ? "clean -qfdx" : "clean -qfd";
}
public Clean(string repo, List<string> 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();
} }
} }
} }

View file

@ -36,14 +36,44 @@ namespace SourceGit.Commands
public bool Exec() public bool Exec()
{ {
Log?.AppendLine($"$ git {Args}\n");
var start = CreateGitStartInfo(); var start = CreateGitStartInfo();
var errs = new List<string>(); var errs = new List<string>();
var proc = new Process() { StartInfo = start }; var proc = new Process() { StartInfo = start };
proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); Log?.AppendLine($"$ git {Args}\n");
proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs);
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);
};
var dummy = null as Process; var dummy = null as Process;
var dummyProcLock = new object(); var dummyProcLock = new object();
@ -192,28 +222,6 @@ namespace SourceGit.Commands
return start; return start;
} }
private void HandleOutput(string line, List<string> 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+%")] [GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS(); private static partial Regex REG_PROGRESS();
} }

View file

@ -4,7 +4,7 @@ namespace SourceGit.Commands
{ {
public class Commit : Command public class Commit : Command
{ {
public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor) public Commit(string repo, string message, bool amend, bool signOff)
{ {
_tmpFile = Path.GetTempFileName(); _tmpFile = Path.GetTempFileName();
File.WriteAllText(_tmpFile, message); File.WriteAllText(_tmpFile, message);
@ -12,10 +12,10 @@ namespace SourceGit.Commands
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"commit --allow-empty --file=\"{_tmpFile}\""; Args = $"commit --allow-empty --file=\"{_tmpFile}\"";
if (amend)
Args += " --amend --no-edit";
if (signOff) if (signOff)
Args += " --signoff"; Args += " --signoff";
if (amend)
Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit";
} }
public bool Run() public bool Run()
@ -34,6 +34,6 @@ namespace SourceGit.Commands
return succ; return succ;
} }
private readonly string _tmpFile; private string _tmpFile = string.Empty;
} }
} }

View file

@ -39,7 +39,7 @@ namespace SourceGit.Commands
foreach (var line in lines) foreach (var line in lines)
ParseLine(line); ParseLine(line);
_changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
return _changes; return _changes;
} }

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain"; Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain";
} }
public int Result() public int Result()

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -28,11 +28,9 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
if (ignoreWhitespace) if (ignoreWhitespace)
Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}"; Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --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 else
Args = $"diff --no-ext-diff --patch --unified={unified} {opt}"; Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --unified={unified} {opt}";
} }
public Models.DiffResult Result() public Models.DiffResult Result()
@ -105,7 +103,7 @@ namespace SourceGit.Commands
} }
else if (line.StartsWith("-size ", StringComparison.Ordinal)) else if (line.StartsWith("-size ", StringComparison.Ordinal))
{ {
_result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
} }
} }
else if (ch == '+') else if (ch == '+')
@ -116,12 +114,12 @@ namespace SourceGit.Commands
} }
else if (line.StartsWith("+size ", StringComparison.Ordinal)) else if (line.StartsWith("+size ", StringComparison.Ordinal))
{ {
_result.LFSDiff.New.Size = long.Parse(line.AsSpan(6)); _result.LFSDiff.New.Size = long.Parse(line.Substring(6));
} }
} }
else if (line.StartsWith(" size ", StringComparison.Ordinal)) else if (line.StartsWith(" size ", StringComparison.Ordinal))
{ {
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
} }
return; return;
} }

View file

@ -1,95 +1,39 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using Avalonia.Threading;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public static class Discard public static class Discard
{ {
/// <summary>
/// Discard all local changes (unstaged & staged)
/// </summary>
/// <param name="repo"></param>
/// <param name="includeIgnored"></param>
/// <param name="log"></param>
public static void All(string repo, bool includeIgnored, Models.ICommandLog log) public static void All(string repo, bool includeIgnored, Models.ICommandLog log)
{ {
var changes = new QueryLocalChanges(repo).Result(); new Restore(repo) { Log = log }.Exec();
try new Clean(repo, includeIgnored) { Log = log }.Exec();
{
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();
}
/// <summary>
/// Discard selected changes (only unstaged).
/// </summary>
/// <param name="repo"></param>
/// <param name="changes"></param>
/// <param name="log"></param>
public static void Changes(string repo, List<Models.Change> changes, Models.ICommandLog log) public static void Changes(string repo, List<Models.Change> changes, Models.ICommandLog log)
{ {
var restores = new List<string>(); var needClean = new List<string>();
var needCheckout = new List<string>();
try
{
foreach (var c in changes) foreach (var c in changes)
{ {
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
{ needClean.Add(c.Path);
var fullPath = Path.Combine(repo, c.Path);
if (Directory.Exists(fullPath))
Directory.Delete(fullPath, true);
else else
File.Delete(fullPath); needCheckout.Add(c.Path);
}
else
{
restores.Add(c.Path);
}
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}");
});
} }
if (restores.Count > 0) for (int i = 0; i < needClean.Count; i += 10)
{ {
var pathSpecFile = Path.GetTempFileName(); var count = Math.Min(10, needClean.Count - i);
File.WriteAllLines(pathSpecFile, restores); new Clean(repo, needClean.GetRange(i, count)) { Log = log }.Exec();
new Restore(repo, pathSpecFile, false) { Log = log }.Exec(); }
File.Delete(pathSpecFile);
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();
} }
} }
} }

View file

@ -27,7 +27,7 @@ namespace SourceGit.Commands
} }
} }
public static void RunAndWait(string repo, string file, string args, Models.ICommandLog log) public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
{ {
var start = new ProcessStartInfo(); var start = new ProcessStartInfo();
start.FileName = file; start.FileName = file;
@ -40,22 +40,20 @@ namespace SourceGit.Commands
start.StandardErrorEncoding = Encoding.UTF8; start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo; start.WorkingDirectory = repo;
log?.AppendLine($"$ {file} {args}\n");
var proc = new Process() { StartInfo = start }; var proc = new Process() { StartInfo = start };
var builder = new StringBuilder(); var builder = new StringBuilder();
proc.OutputDataReceived += (_, e) => proc.OutputDataReceived += (_, e) =>
{ {
if (e.Data != null) if (e.Data != null)
log?.AppendLine(e.Data); outputHandler?.Invoke(e.Data);
}; };
proc.ErrorDataReceived += (_, e) => proc.ErrorDataReceived += (_, e) =>
{ {
if (e.Data != null) if (e.Data != null)
{ {
log?.AppendLine(e.Data); outputHandler?.Invoke(e.Data);
builder.AppendLine(e.Data); builder.AppendLine(e.Data);
} }
}; };

View file

@ -6,7 +6,6 @@
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Editor = EditorType.None;
Args = $"format-patch {commit} -1 --output=\"{saveTo}\""; Args = $"format-patch {commit} -1 --output=\"{saveTo}\"";
} }
} }

View file

@ -1,12 +1,52 @@
using System.Text; using System;
using System.Collections.Generic;
using Avalonia.Threading; using Avalonia.Threading;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public static class GitFlow public static class GitFlow
{ {
public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log) public class BranchDetectResult
{ {
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<Models.Branch> branches)
{
var localBrancheNames = new HashSet<string>();
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<Models.Branch> 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); var config = new Config(repo);
config.Set("gitflow.branch.master", master); config.Set("gitflow.branch.master", master);
config.Set("gitflow.branch.develop", develop); config.Set("gitflow.branch.develop", develop);
@ -25,68 +65,103 @@ namespace SourceGit.Commands
return init.Exec(); return init.Exec();
} }
public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) public static string GetPrefix(string repo, string type)
{ {
var start = new Command(); return new Config(repo).Get($"gitflow.prefix.{type}");
start.WorkingDirectory = repo; }
start.Context = repo;
switch (type) public static BranchDetectResult DetectType(string repo, List<Models.Branch> branches, string branch)
{ {
case Models.GitFlowBranchType.Feature: var rs = new BranchDetectResult();
start.Args = $"flow feature start {name}"; var localBrancheNames = new HashSet<string>();
break; foreach (var b in branches)
case Models.GitFlowBranchType.Release: {
start.Args = $"flow release start {name}"; if (b.IsLocal)
break; localBrancheNames.Add(b.Name);
case Models.GitFlowBranchType.Hotfix: }
start.Args = $"flow hotfix start {name}";
break; var config = new Config(repo).ListAll();
default: if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); 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; return false;
} }
var start = new Command();
start.WorkingDirectory = repo;
start.Context = repo;
start.Args = $"flow {type} start {name}";
start.Log = log; start.Log = log;
return start.Exec(); return start.Exec();
} }
public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) public static bool Finish(string repo, string type, string name, bool keepBranch, Models.ICommandLog log)
{ {
var builder = new StringBuilder(); if (!SUPPORTED_BRANCH_TYPES.Contains(type))
builder.Append("flow "); {
Dispatcher.UIThread.Post(() =>
{
App.RaiseException(repo, "Bad branch type!!!");
});
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; return false;
} }
builder.Append(" finish "); var option = keepBranch ? "-k" : string.Empty;
if (squash)
builder.Append("--squash ");
if (push)
builder.Append("--push ");
if (keepBranch)
builder.Append("-k ");
builder.Append(name);
var finish = new Command(); var finish = new Command();
finish.WorkingDirectory = repo; finish.WorkingDirectory = repo;
finish.Context = repo; finish.Context = repo;
finish.Args = builder.ToString(); finish.Args = $"flow {type} finish {option} {name}";
finish.Log = log; finish.Log = log;
return finish.Exec(); return finish.Exec();
} }
private static readonly List<string> SUPPORTED_BRANCH_TYPES = new List<string>()
{
"feature",
"release",
"bugfix",
"hotfix",
"support",
};
} }
} }

View file

@ -8,14 +8,7 @@ namespace SourceGit.Commands
{ {
var file = Path.Combine(repo, ".gitignore"); var file = Path.Combine(repo, ".gitignore");
if (!File.Exists(file)) if (!File.Exists(file))
{
File.WriteAllLines(file, [pattern]); File.WriteAllLines(file, [pattern]);
return;
}
var org = File.ReadAllText(file);
if (!org.EndsWith('\n'))
File.AppendAllLines(file, ["", pattern]);
else else
File.AppendAllLines(file, [pattern]); File.AppendAllLines(file, [pattern]);
} }

View file

@ -11,7 +11,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- \"{path}\""; Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
RaiseError = false; RaiseError = false;
} }

View file

@ -10,7 +10,7 @@ namespace SourceGit.Commands
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] [GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK(); private static partial Regex REG_LOCK();
private class SubCmd : Command class SubCmd : Command
{ {
public SubCmd(string repo, string args, Models.ICommandLog log) public SubCmd(string repo, string args, Models.ICommandLog log)
{ {

View file

@ -5,20 +5,11 @@ namespace SourceGit.Commands
{ {
public class Merge : Command public class Merge : Command
{ {
public Merge(string repo, string source, string mode, bool edit) public Merge(string repo, string source, string mode)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Editor = EditorType.CoreEditor; Args = $"merge --progress {source} {mode}";
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<string> targets, bool autoCommit, string strategy) public Merge(string repo, List<string> targets, bool autoCommit, string strategy)

View file

@ -24,7 +24,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath)) if (!File.Exists(toolPath))
{ {
Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!")); Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!"));
return false; return false;
} }
@ -54,7 +54,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath)) if (!File.Exists(toolPath))
{ {
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!")); Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!"));
return false; return false;
} }

View file

@ -2,7 +2,7 @@
{ {
public class Pull : Command public class Pull : Command
{ {
public Pull(string repo, string remote, string branch, bool useRebase) public Pull(string repo, string remote, string branch, bool useRebase, bool noTags)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
@ -12,6 +12,9 @@
if (useRebase) if (useRebase)
Args += "--rebase=true "; Args += "--rebase=true ";
if (noTags)
Args += "--no-tags ";
Args += $"{remote} {branch}"; Args += $"{remote} {branch}";
} }
} }

View file

@ -14,20 +14,18 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\"";
} }
public List<Models.Branch> Result(out int localBranchesCount) public List<Models.Branch> Result()
{ {
localBranchesCount = 0;
var branches = new List<Models.Branch>(); var branches = new List<Models.Branch>();
var rs = ReadToEnd(); var rs = ReadToEnd();
if (!rs.IsSuccess) if (!rs.IsSuccess)
return branches; return branches;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var remoteHeads = new Dictionary<string, string>(); var remoteBranches = new HashSet<string>();
foreach (var line in lines) foreach (var line in lines)
{ {
var b = ParseLine(line); var b = ParseLine(line);
@ -35,27 +33,14 @@ namespace SourceGit.Commands
{ {
branches.Add(b); branches.Add(b);
if (!b.IsLocal) if (!b.IsLocal)
remoteHeads.Add(b.FullName, b.Head); remoteBranches.Add(b.FullName);
else
localBranchesCount++;
} }
} }
foreach (var b in branches) foreach (var b in branches)
{ {
if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream)) 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; return branches;
@ -64,7 +49,7 @@ namespace SourceGit.Commands
private Models.Branch ParseLine(string line) private Models.Branch ParseLine(string line)
{ {
var parts = line.Split('\0'); var parts = line.Split('\0');
if (parts.Length != 6) if (parts.Length != 5)
return null; return null;
var branch = new Models.Branch(); var branch = new Models.Branch();
@ -98,16 +83,14 @@ namespace SourceGit.Commands
} }
branch.FullName = refName; branch.FullName = refName;
branch.CommitterDate = ulong.Parse(parts[1]); branch.Head = parts[1];
branch.Head = parts[2]; branch.IsCurrent = parts[2] == "*";
branch.IsCurrent = parts[3] == "*"; branch.Upstream = parts[3];
branch.Upstream = parts[4];
branch.IsUpstreamGone = false; branch.IsUpstreamGone = false;
if (!branch.IsLocal || if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
string.IsNullOrEmpty(branch.Upstream) || branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();
string.IsNullOrEmpty(parts[5]) || else
parts[5].Equals("=", StringComparison.Ordinal))
branch.TrackStatus = new Models.BranchTrackStatus(); branch.TrackStatus = new Models.BranchTrackStatus();
return branch; return branch;

View file

@ -26,7 +26,11 @@ namespace SourceGit.Commands
{ {
search += $"-i --committer=\"{filter}\""; search += $"-i --committer=\"{filter}\"";
} }
else if (method == Models.CommitSearchMethod.ByMessage) else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
{ {
var argsBuilder = new StringBuilder(); var argsBuilder = new StringBuilder();
argsBuilder.Append(search); argsBuilder.Append(search);
@ -41,18 +45,10 @@ namespace SourceGit.Commands
search = argsBuilder.ToString(); search = argsBuilder.ToString();
} }
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
{
search = $"-G\"{filter}\"";
}
WorkingDirectory = repo; WorkingDirectory = repo;
Context = 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; _findFirstMerged = false;
} }

View file

@ -90,6 +90,6 @@ namespace SourceGit.Commands
private List<Models.InteractiveCommit> _commits = []; private List<Models.InteractiveCommit> _commits = [];
private Models.InteractiveCommit _current = null; private Models.InteractiveCommit _current = null;
private readonly string _boundary; private string _boundary = "";
} }
} }

View file

@ -35,39 +35,5 @@ namespace SourceGit.Commands
return stream; 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;
}
} }
} }

View file

@ -16,6 +16,9 @@ namespace SourceGit.Commands
public long Result() public long Result()
{ {
if (_result != 0)
return _result;
var rs = ReadToEnd(); var rs = ReadToEnd();
if (rs.IsSuccess) if (rs.IsSuccess)
{ {
@ -26,5 +29,7 @@ namespace SourceGit.Commands
return 0; return 0;
} }
private readonly long _result = 0;
} }
} }

View file

@ -2,8 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Avalonia.Threading;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public partial class QueryLocalChanges : Command public partial class QueryLocalChanges : Command
@ -24,10 +22,7 @@ namespace SourceGit.Commands
var outs = new List<Models.Change>(); var outs = new List<Models.Change>();
var rs = ReadToEnd(); var rs = ReadToEnd();
if (!rs.IsSuccess) if (!rs.IsSuccess)
{
Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr));
return outs; return outs;
}
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
@ -122,36 +117,37 @@ namespace SourceGit.Commands
case "CD": case "CD":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
break; 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": case "DD":
change.ConflictReason = Models.ConflictReason.BothDeleted; change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "AU": case "AU":
change.ConflictReason = Models.ConflictReason.AddedByUs; change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "UD": case "UD":
change.ConflictReason = Models.ConflictReason.DeletedByThem; change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "UA": case "UA":
change.ConflictReason = Models.ConflictReason.AddedByThem; change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "DU": case "DU":
change.ConflictReason = Models.ConflictReason.DeletedByUs; change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "AA": case "AA":
change.ConflictReason = Models.ConflictReason.BothAdded; change.Set(Models.ChangeState.Added, Models.ChangeState.Added);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "UU": case "UU":
change.ConflictReason = Models.ConflictReason.BothModified; change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged);
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break; break;
case "??": case "??":
change.Set(Models.ChangeState.None, Models.ChangeState.Untracked); change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked);
break;
default:
break; break;
} }

View file

@ -6,25 +6,23 @@ namespace SourceGit.Commands
{ {
public partial class QueryStagedChangesWithAmend : Command public partial class QueryStagedChangesWithAmend : Command
{ {
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")] [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")]
private static partial Regex REG_FORMAT1(); private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")] [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")]
private static partial Regex REG_FORMAT2(); private static partial Regex REG_FORMAT2();
public QueryStagedChangesWithAmend(string repo, string parent) public QueryStagedChangesWithAmend(string repo)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff-index --cached -M {parent}"; Args = "diff-index --cached -M HEAD^";
_parent = parent;
} }
public List<Models.Change> Result() public List<Models.Change> Result()
{ {
var rs = ReadToEnd(); var rs = ReadToEnd();
if (!rs.IsSuccess) if (rs.IsSuccess)
return []; {
var changes = new List<Models.Change>(); var changes = new List<Models.Change>();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
@ -39,7 +37,6 @@ namespace SourceGit.Commands
{ {
FileMode = match.Groups[1].Value, FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value, ObjectHash = match.Groups[2].Value,
ParentSHA = _parent,
}, },
}; };
change.Set(Models.ChangeState.Renamed); change.Set(Models.ChangeState.Renamed);
@ -57,7 +54,6 @@ namespace SourceGit.Commands
{ {
FileMode = match.Groups[1].Value, FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value, ObjectHash = match.Groups[2].Value,
ParentSHA = _parent,
}, },
}; };
@ -79,6 +75,9 @@ namespace SourceGit.Commands
case "T": case "T":
change.Set(Models.ChangeState.TypeChanged); change.Set(Models.ChangeState.TypeChanged);
break; break;
case "U":
change.Set(Models.ChangeState.Unmerged);
break;
} }
changes.Add(change); changes.Add(change);
} }
@ -87,6 +86,7 @@ namespace SourceGit.Commands
return changes; return changes;
} }
private readonly string _parent; return [];
}
} }
} }

View file

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
/// <summary>
/// Query stash changes. Requires git >= 2.32.0
/// </summary>
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<Models.Change> Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
return [];
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var outs = new List<Models.Change>();
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;
}
}
}

View file

@ -9,7 +9,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"stash list -z --no-show-signature --format=\"%H%n%P%n%ct%n%gd%n%B\""; Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s";
} }
public List<Models.Stash> Result() public List<Models.Stash> Result()
@ -19,50 +19,55 @@ namespace SourceGit.Commands
if (!rs.IsSuccess) if (!rs.IsSuccess)
return outs; return outs;
var items = rs.StdOut.Split('\0', StringSplitOptions.RemoveEmptyEntries);
foreach (var item in items)
{
var current = new Models.Stash();
var nextPartIdx = 0; var nextPartIdx = 0;
var start = 0; var start = 0;
var end = item.IndexOf('\n', start); var end = rs.StdOut.IndexOf('\n', start);
while (end > 0 && nextPartIdx < 4) while (end > 0)
{ {
var line = item.Substring(start, end - start); var line = rs.StdOut.Substring(start, end - start);
switch (nextPartIdx) switch (nextPartIdx)
{ {
case 0: case 0:
current.SHA = line; _current = new Models.Stash() { SHA = line };
outs.Add(_current);
break; break;
case 1: case 1:
if (line.Length > 6) ParseParent(line);
current.Parents.AddRange(line.Split(' ', StringSplitOptions.RemoveEmptyEntries));
break; break;
case 2: case 2:
current.Time = ulong.Parse(line); _current.Time = ulong.Parse(line);
break; break;
case 3: case 3:
current.Name = line; _current.Name = line;
break;
case 4:
_current.Message = line;
break; break;
} }
nextPartIdx++; nextPartIdx++;
if (nextPartIdx > 4)
nextPartIdx = 0;
start = end + 1; start = end + 1;
if (start >= item.Length - 1) end = rs.StdOut.IndexOf('\n', start);
break;
end = item.IndexOf('\n', start);
} }
if (start < item.Length) if (start < rs.StdOut.Length)
current.Message = item.Substring(start); _current.Message = rs.StdOut.Substring(start);
outs.Add(current);
}
return outs; 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;
} }
} }

View file

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -7,12 +6,12 @@ namespace SourceGit.Commands
{ {
public partial class QuerySubmodules : Command public partial class QuerySubmodules : Command
{ {
[GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
private static partial Regex REG_FORMAT_STATUS(); private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
private static partial Regex REG_FORMAT2();
[GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")]
private static partial Regex REG_FORMAT_DIRTY(); private static partial Regex REG_FORMAT_STATUS();
[GeneratedRegex(@"^submodule\.(\S*)\.(\w+)=(.*)$")]
private static partial Regex REG_FORMAT_MODULE_INFO();
public QuerySubmodules(string repo) public QuerySubmodules(string repo)
{ {
@ -26,117 +25,52 @@ namespace SourceGit.Commands
var submodules = new List<Models.Submodule>(); var submodules = new List<Models.Submodule>();
var rs = ReadToEnd(); var rs = ReadToEnd();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var builder = new StringBuilder();
var map = new Dictionary<string, Models.Submodule>(); var lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
var needCheckLocalChanges = false;
foreach (var line in lines) foreach (var line in lines)
{ {
var match = REG_FORMAT_STATUS().Match(line); var match = REG_FORMAT1().Match(line);
if (match.Success) if (match.Success)
{ {
var stat = match.Groups[1].Value; var path = match.Groups[1].Value;
var sha = match.Groups[2].Value; builder.Append($"\"{path}\" ");
var path = match.Groups[3].Value; submodules.Add(new Models.Submodule() { Path = path });
continue;
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); match = REG_FORMAT2().Match(line);
submodules.Add(module); if (match.Success)
{
var path = match.Groups[1].Value;
builder.Append($"\"{path}\" ");
submodules.Add(new Models.Submodule() { Path = path });
} }
} }
if (submodules.Count > 0) if (submodules.Count > 0)
{ {
Args = "config --file .gitmodules --list"; Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
rs = ReadToEnd();
if (rs.IsSuccess)
{
var modules = new Dictionary<string, ModuleInfo>();
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(); rs = ReadToEnd();
if (!rs.IsSuccess) if (!rs.IsSuccess)
return submodules; return submodules;
lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var dirty = new HashSet<string>();
lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
var match = REG_FORMAT_DIRTY().Match(line); var match = REG_FORMAT_STATUS().Match(line);
if (match.Success) if (match.Success)
{ {
var path = match.Groups[1].Value; var path = match.Groups[1].Value;
if (map.TryGetValue(path, out var m)) dirty.Add(path);
m.Status = Models.SubmoduleStatus.Modified;
} }
} }
foreach (var submodule in submodules)
submodule.IsDirty = dirty.Contains(submodule.Path);
} }
return submodules; return submodules;
} }
private class ModuleInfo
{
public string Path { get; set; } = string.Empty;
public string URL { get; set; } = string.Empty;
}
} }
} }

View file

@ -11,7 +11,7 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
WorkingDirectory = repo; WorkingDirectory = repo;
Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\"";
} }
public List<Models.Tag> Result() public List<Models.Tag> Result()
@ -24,22 +24,17 @@ namespace SourceGit.Commands
var records = rs.StdOut.Split(_boundary, StringSplitOptions.RemoveEmptyEntries); var records = rs.StdOut.Split(_boundary, StringSplitOptions.RemoveEmptyEntries);
foreach (var record in records) foreach (var record in records)
{ {
var subs = record.Split('\0'); var subs = record.Split('\0', StringSplitOptions.None);
if (subs.Length != 6) if (subs.Length != 5)
continue; continue;
var name = subs[0].Substring(10); var message = subs[4].Trim();
var message = subs[5].Trim();
if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal))
message = null;
tags.Add(new Models.Tag() tags.Add(new Models.Tag()
{ {
Name = name, Name = subs[0].Substring(10),
IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal), SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3], CreatorDate = ulong.Parse(subs[3]),
CreatorDate = ulong.Parse(subs[4]), Message = string.IsNullOrEmpty(message) ? null : message,
Message = message,
}); });
} }

View file

@ -1,40 +0,0 @@
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<string> Result()
{
var submodules = new List<string>();
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;
}
}
}

View file

@ -1,7 +1,33 @@
namespace SourceGit.Commands using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{ {
public class Reset : Command public class Reset : Command
{ {
public Reset(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "reset";
}
public Reset(string repo, List<Models.Change> 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) public Reset(string repo, string revision, string mode)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;

View file

@ -1,52 +1,29 @@
using System.Text; using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public class Restore : Command public class Restore : Command
{ {
/// <summary> public Restore(string repo)
/// Only used for single staged change. {
/// </summary> WorkingDirectory = repo;
/// <param name="repo"></param> Context = repo;
/// <param name="stagedChange"></param> Args = "restore . --source=HEAD --staged --worktree --recurse-submodules";
public Restore(string repo, Models.Change stagedChange) }
public Restore(string repo, List<string> files, string extra)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
var builder = new StringBuilder(); StringBuilder 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();
}
/// <summary>
/// Restore changes given in a path-spec file.
/// </summary>
/// <param name="repo"></param>
/// <param name="pathspecFile"></param>
/// <param name="isStaged"></param>
public Restore(string repo, string pathspecFile, bool isStaged)
{
WorkingDirectory = repo;
Context = repo;
var builder = new StringBuilder();
builder.Append("restore "); builder.Append("restore ");
builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules "); if (!string.IsNullOrEmpty(extra))
builder.Append("--pathspec-from-file=\""); builder.Append(extra).Append(" ");
builder.Append(pathspecFile); builder.Append("--");
builder.Append('"'); foreach (var f in files)
builder.Append(' ').Append('"').Append(f).Append('"');
Args = builder.ToString(); Args = builder.ToString();
} }
} }

View file

@ -10,15 +10,15 @@ namespace SourceGit.Commands
{ {
public static void Run(string repo, string revision, string file, string saveTo) 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(); var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result();
if (isLFSFiltered) if (isLFSFiltered)
{ {
var pointerStream = QueryFileContent.Run(repo, revision, file); var tmpFile = saveTo + ".tmp";
ExecCmd(repo, $"lfs smudge", saveTo, pointerStream); if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile))
{
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
}
File.Delete(tmpFile);
} }
else else
{ {
@ -26,7 +26,7 @@ namespace SourceGit.Commands
} }
} }
private static void ExecCmd(string repo, string args, string outputFile, Stream input = null) private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
{ {
var starter = new ProcessStartInfo(); var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo; starter.WorkingDirectory = repo;
@ -45,11 +45,27 @@ namespace SourceGit.Commands
{ {
var proc = new Process() { StartInfo = starter }; var proc = new Process() { StartInfo = starter };
proc.Start(); proc.Start();
if (input != null)
proc.StandardInput.Write(new StreamReader(input).ReadToEnd()); if (inputFile != null)
{
using (StreamReader sr = new StreamReader(inputFile))
{
while (true)
{
var line = sr.ReadLine();
if (line == null)
break;
proc.StandardInput.WriteLine(line);
}
}
}
proc.StandardOutput.BaseStream.CopyTo(sw); proc.StandardOutput.BaseStream.CopyTo(sw);
proc.WaitForExit(); proc.WaitForExit();
var rs = proc.ExitCode == 0;
proc.Close(); proc.Close();
return rs;
} }
catch (Exception e) catch (Exception e)
{ {
@ -57,6 +73,7 @@ namespace SourceGit.Commands
{ {
App.RaiseException(repo, "Save file failed: " + e.Message); App.RaiseException(repo, "Save file failed: " + e.Message);
}); });
return false;
} }
} }
} }

View file

@ -40,7 +40,7 @@ namespace SourceGit.Commands
if (dateEndIdx == -1) if (dateEndIdx == -1)
return; return;
var dateStr = line.AsSpan(0, dateEndIdx); var dateStr = line.AsSpan().Slice(0, dateEndIdx);
if (double.TryParse(dateStr, out var date)) if (double.TryParse(dateStr, out var date))
statistics.AddCommit(line.Substring(dateEndIdx + 1), date); statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
} }

View file

@ -1,7 +1,4 @@
using System.Collections.Generic; namespace SourceGit.Commands
using System.Text;
namespace SourceGit.Commands
{ {
public class Submodule : Command public class Submodule : Command
{ {
@ -13,7 +10,7 @@ namespace SourceGit.Commands
public bool Add(string url, string relativePath, bool recursive) public bool Add(string url, string relativePath, bool recursive)
{ {
Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\""; Args = $"submodule add {url} \"{relativePath}\"";
if (!Exec()) if (!Exec())
return false; return false;
@ -29,37 +26,29 @@ namespace SourceGit.Commands
} }
} }
public bool Update(List<string> modules, bool init, bool recursive, bool useRemote = false) public bool Update(string module, bool init, bool recursive, bool useRemote)
{ {
var builder = new StringBuilder(); Args = "submodule update";
builder.Append("submodule update");
if (init) if (init)
builder.Append(" --init"); Args += " --init";
if (recursive) if (recursive)
builder.Append(" --recursive"); Args += " --recursive";
if (useRemote) if (useRemote)
builder.Append(" --remote"); Args += " --remote";
if (modules.Count > 0) if (!string.IsNullOrEmpty(module))
{ Args += $" -- \"{module}\"";
builder.Append(" --");
foreach (var module in modules)
builder.Append($" \"{module}\"");
}
Args = builder.ToString();
return Exec(); return Exec();
} }
public bool Deinit(string module, bool force) public bool Delete(string relativePath)
{ {
Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\""; Args = $"submodule deinit -f \"{relativePath}\"";
return Exec(); if (!Exec())
} return false;
public bool Delete(string module) Args = $"rm -rf \"{relativePath}\"";
{
Args = $"rm -rf \"{module}\"";
return Exec(); return Exec();
} }
} }

View file

@ -9,7 +9,7 @@ namespace SourceGit.Commands
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"tag --no-sign {name} {basedOn}"; cmd.Args = $"tag {name} {basedOn}";
cmd.Log = log; cmd.Log = log;
return cmd.Exec(); return cmd.Exec();
} }
@ -28,13 +28,12 @@ namespace SourceGit.Commands
string tmp = Path.GetTempFileName(); string tmp = Path.GetTempFileName();
File.WriteAllText(tmp, message); File.WriteAllText(tmp, message);
cmd.Args += $"-F \"{tmp}\""; cmd.Args += $"-F \"{tmp}\"";
}
var succ = cmd.Exec(); else
File.Delete(tmp); {
return succ; cmd.Args += $"-m {name}";
} }
cmd.Args += $"-m {name}";
return cmd.Exec(); return cmd.Exec();
} }

View file

@ -23,11 +23,13 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t"); _patchBuilder.Append("\t");
_patchBuilder.Append(c.OriginalPath); _patchBuilder.Append(c.OriginalPath);
_patchBuilder.Append("\n");
} }
else if (c.Index == Models.ChangeState.Added) else if (c.Index == Models.ChangeState.Added)
{ {
_patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); _patchBuilder.Append("0 0000000000000000000000000000000000000000\t");
_patchBuilder.Append(c.Path); _patchBuilder.Append(c.Path);
_patchBuilder.Append("\n");
} }
else if (c.Index == Models.ChangeState.Deleted) else if (c.Index == Models.ChangeState.Deleted)
{ {
@ -35,6 +37,7 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t"); _patchBuilder.Append("\t");
_patchBuilder.Append(c.Path); _patchBuilder.Append(c.Path);
_patchBuilder.Append("\n");
} }
else else
{ {
@ -43,11 +46,10 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t"); _patchBuilder.Append("\t");
_patchBuilder.Append(c.Path); _patchBuilder.Append(c.Path);
}
_patchBuilder.Append("\n"); _patchBuilder.Append("\n");
} }
} }
}
public bool Exec() public bool Exec()
{ {

12
src/Commands/UpdateRef.cs Normal file
View file

@ -0,0 +1,12 @@
namespace SourceGit.Commands
{
public class UpdateRef : Command
{
public UpdateRef(string repo, string refName, string toRevision)
{
WorkingDirectory = repo;
Context = repo;
Args = $"update-ref {refName} {toRevision}";
}
}
}

View file

@ -7,11 +7,8 @@ namespace SourceGit.Converters
{ {
public static class ListConverters public static class ListConverters
{ {
public static readonly FuncValueConverter<IList, string> Count =
new FuncValueConverter<IList, string>(v => v == null ? "0" : $"{v.Count}");
public static readonly FuncValueConverter<IList, string> ToCount = public static readonly FuncValueConverter<IList, string> ToCount =
new FuncValueConverter<IList, string>(v => v == null ? "(0)" : $"({v.Count})"); new FuncValueConverter<IList, string>(v => v == null ? " (0)" : $" ({v.Count})");
public static readonly FuncValueConverter<IList, bool> IsNullOrEmpty = public static readonly FuncValueConverter<IList, bool> IsNullOrEmpty =
new FuncValueConverter<IList, bool>(v => v == null || v.Count == 0); new FuncValueConverter<IList, bool>(v => v == null || v.Count == 0);

View file

@ -1,27 +0,0 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace SourceGit.Converters
{
public static class ObjectConverters
{
public class IsTypeOfConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || parameter == null)
return false;
return value.GetType().IsAssignableTo((Type)parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return new NotImplementedException();
}
}
public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter();
}
}

View file

@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
@ -22,7 +22,7 @@ namespace SourceGit.Converters
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length;
if (v.StartsWith(home, StringComparison.Ordinal)) if (v.StartsWith(home, StringComparison.Ordinal))
return $"~{v.AsSpan(prefixLen)}"; return "~" + v.Substring(prefixLen);
return v; return v;
}); });

View file

@ -17,7 +17,7 @@ namespace SourceGit.Models
{ {
public interface IAvatarHost public interface IAvatarHost
{ {
void OnAvatarResourceChanged(string email, Bitmap image); void OnAvatarResourceChanged(string email);
} }
public partial class AvatarManager public partial class AvatarManager
@ -26,7 +26,10 @@ namespace SourceGit.Models
{ {
get get
{ {
return _instance ??= new AvatarManager(); if (_instance == null)
_instance = new AvatarManager();
return _instance;
} }
} }
@ -35,7 +38,7 @@ namespace SourceGit.Models
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")] [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")]
private static partial Regex REG_GITHUB_USER_EMAIL(); private static partial Regex REG_GITHUB_USER_EMAIL();
private readonly Lock _synclock = new(); private object _synclock = new object();
private string _storePath; private string _storePath;
private List<IAvatarHost> _avatars = new List<IAvatarHost>(); private List<IAvatarHost> _avatars = new List<IAvatarHost>();
private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>(); private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
@ -116,7 +119,7 @@ namespace SourceGit.Models
Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.InvokeAsync(() =>
{ {
_resources[email] = img; _resources[email] = img;
NotifyResourceChanged(email, img); NotifyResourceChanged(email);
}); });
} }
@ -141,13 +144,14 @@ namespace SourceGit.Models
if (_defaultAvatars.Contains(email)) if (_defaultAvatars.Contains(email))
return null; return null;
if (_resources.ContainsKey(email))
_resources.Remove(email); _resources.Remove(email);
var localFile = Path.Combine(_storePath, GetEmailHash(email)); var localFile = Path.Combine(_storePath, GetEmailHash(email));
if (File.Exists(localFile)) if (File.Exists(localFile))
File.Delete(localFile); File.Delete(localFile);
NotifyResourceChanged(email, null); NotifyResourceChanged(email);
} }
else else
{ {
@ -175,40 +179,13 @@ namespace SourceGit.Models
lock (_synclock) lock (_synclock)
{ {
if (!_requesting.Contains(email))
_requesting.Add(email); _requesting.Add(email);
} }
return null; 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) private void LoadDefaultAvatar(string key, string img)
{ {
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute)); var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute));
@ -219,17 +196,19 @@ namespace SourceGit.Models
private string GetEmailHash(string email) private string GetEmailHash(string email)
{ {
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim(); var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered)); var hash = MD5.HashData(Encoding.Default.GetBytes(lowered).AsSpan());
var builder = new StringBuilder(hash.Length * 2); var builder = new StringBuilder(hash.Length * 2);
foreach (var c in hash) foreach (var c in hash)
builder.Append(c.ToString("x2")); builder.Append(c.ToString("x2"));
return builder.ToString(); return builder.ToString();
} }
private void NotifyResourceChanged(string email, Bitmap image) private void NotifyResourceChanged(string email)
{ {
foreach (var avatar in _avatars) foreach (var avatar in _avatars)
avatar.OnAvatarResourceChanged(email, image); {
avatar.OnAvatarResourceChanged(email);
}
} }
} }
} }

View file

@ -1,35 +0,0 @@
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<string> Bads
{
get;
set;
} = [];
public HashSet<string> Goods
{
get;
set;
} = [];
}
}

View file

@ -23,17 +23,10 @@ namespace SourceGit.Models
} }
} }
public enum BranchSortMode
{
Name = 0,
CommitterDate,
}
public class Branch public class Branch
{ {
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; set; } public string FullName { get; set; }
public ulong CommitterDate { get; set; }
public string Head { get; set; } public string Head { get; set; }
public bool IsLocal { get; set; } public bool IsLocal { get; set; }
public bool IsCurrent { get; set; } public bool IsCurrent { get; set; }

View file

@ -18,27 +18,14 @@ namespace SourceGit.Models
Deleted, Deleted,
Renamed, Renamed,
Copied, Copied,
Untracked, Unmerged,
Conflicted, Untracked
}
public enum ConflictReason
{
None,
BothDeleted,
AddedByUs,
DeletedByThem,
AddedByThem,
DeletedByUs,
BothAdded,
BothModified,
} }
public class ChangeDataForAmend public class ChangeDataForAmend
{ {
public string FileMode { get; set; } = ""; public string FileMode { get; set; } = "";
public string ObjectHash { get; set; } = ""; public string ObjectHash { get; set; } = "";
public string ParentSHA { get; set; } = "";
} }
public class Change public class Change
@ -48,14 +35,20 @@ namespace SourceGit.Models
public string Path { get; set; } = ""; public string Path { get; set; } = "";
public string OriginalPath { get; set; } = ""; public string OriginalPath { get; set; } = "";
public ChangeDataForAmend DataForAmend { get; set; } = null; public ChangeDataForAmend DataForAmend { get; set; } = null;
public ConflictReason ConflictReason { get; set; } = ConflictReason.None;
public bool IsConflicted => WorkTree == ChangeState.Conflicted; public bool IsConflict
public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason]; {
public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason]; get
{
public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree]; if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged)
public string IndexDesc => TYPE_DESCS[(int)Index]; return true;
if (Index == ChangeState.Added && WorkTree == ChangeState.Added)
return true;
if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted)
return true;
return false;
}
}
public void Set(ChangeState index, ChangeState workTree = ChangeState.None) public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{ {
@ -83,44 +76,8 @@ namespace SourceGit.Models
if (Path[0] == '"') if (Path[0] == '"')
Path = Path.Substring(1, Path.Length - 2); Path = Path.Substring(1, Path.Length - 2);
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"')
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2); 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"
];
} }
} }

View file

@ -13,14 +13,10 @@ namespace SourceGit.Models
ByCommitter, ByCommitter,
ByMessage, ByMessage,
ByFile, ByFile,
ByContent,
} }
public class Commit public class Commit
{ {
// As retrieved by: git mktree </dev/null
public const string EmptyTreeSHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
public static double OpacityForNotMerged public static double OpacityForNotMerged
{ {
get; get;
@ -33,14 +29,14 @@ namespace SourceGit.Models
public User Committer { get; set; } = User.Invalid; public User Committer { get; set; } = User.Invalid;
public ulong CommitterTime { get; set; } = 0; public ulong CommitterTime { get; set; } = 0;
public string Subject { get; set; } = string.Empty; public string Subject { get; set; } = string.Empty;
public List<string> Parents { get; set; } = new(); public List<string> Parents { get; set; } = new List<string>();
public List<Decorator> Decorators { get; set; } = new(); public List<Decorator> Decorators { get; set; } = new List<Decorator>();
public bool HasDecorators => Decorators.Count > 0; public bool HasDecorators => Decorators.Count > 0;
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly); public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly); public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public bool IsMerged { get; set; } = false; public bool IsMerged { get; set; } = false;
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
@ -49,7 +45,7 @@ namespace SourceGit.Models
public int Color { get; set; } = 0; public int Color { get; set; } = 0;
public double Opacity => IsMerged ? 1 : OpacityForNotMerged; public double Opacity => IsMerged ? 1 : OpacityForNotMerged;
public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular; public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular;
public Thickness Margin { get; set; } = new(0); public Thickness Margin { get; set; } = new Thickness(0);
public IBrush Brush => CommitGraph.Pens[Color].Brush; public IBrush Brush => CommitGraph.Pens[Color].Brush;
public void ParseDecorators(string data) public void ParseDecorators(string data)
@ -113,7 +109,7 @@ namespace SourceGit.Models
if (l.Type != r.Type) if (l.Type != r.Type)
return (int)l.Type - (int)r.Type; return (int)l.Type - (int)r.Type;
else else
return NumericSort.Compare(l.Name, r.Name); return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
}); });
} }
} }
@ -121,6 +117,6 @@ namespace SourceGit.Models
public class CommitFullMessage public class CommitFullMessage
{ {
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
public InlineElementCollector Inlines { get; set; } = new(); public List<Hyperlink> Links { get; set; } = [];
} }
} }

View file

@ -64,8 +64,8 @@ namespace SourceGit.Models
{ {
const double unitWidth = 12; const double unitWidth = 12;
const double halfWidth = 6; const double halfWidth = 6;
const double unitHeight = 1; const double unitHeight = 28;
const double halfHeight = 0.5; const double halfHeight = 14;
var temp = new CommitGraph(); var temp = new CommitGraph();
var unsolved = new List<PathHelper>(); var unsolved = new List<PathHelper>();

View file

@ -1,49 +1,8 @@
using System; namespace SourceGit.Models
using System.Collections.Generic;
namespace SourceGit.Models
{ {
public class CommitLink public class CommitLink
{ {
public string Name { get; set; } = null; public string Name { get; set; } = null;
public string URLPrefix { get; set; } = null; public string URLPrefix { get; set; } = null;
public CommitLink(string name, string prefix)
{
Name = name;
URLPrefix = prefix;
}
public static List<CommitLink> Get(List<Remote> remotes)
{
var outs = new List<CommitLink>();
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;
}
} }
} }

View file

@ -4,7 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models namespace SourceGit.Models
{ {
public class CommitTemplate : ObservableObject public partial class CommitTemplate : ObservableObject
{ {
public string Name public string Name
{ {

View file

@ -4,24 +4,25 @@ namespace SourceGit.Models
{ {
public class ConventionalCommitType public class ConventionalCommitType
{ {
public string Name { get; set; } public string Name { get; set; } = string.Empty;
public string Type { get; set; } public string Type { get; set; } = string.Empty;
public string Description { get; set; } public string Description { get; set; } = string.Empty;
public static readonly List<ConventionalCommitType> Supported = [ public static readonly List<ConventionalCommitType> Supported = new List<ConventionalCommitType>()
new("Features", "feat", "Adding a new feature"), {
new("Bug Fixes", "fix", "Fixing a bug"), new ConventionalCommitType("Features", "feat", "Adding a new feature"),
new("Work In Progress", "wip", "Still being developed and not yet complete"), new ConventionalCommitType("Bug Fixes", "fix", "Fixing a bug"),
new("Reverts", "revert", "Undoing a previous commit"), new ConventionalCommitType("Work In Progress", "wip", "Still being developed and not yet complete"),
new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"), new ConventionalCommitType("Reverts", "revert", "Undoing a previous commit"),
new("Performance Improvements", "perf", "Improves performance"), new ConventionalCommitType("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"),
new("Builds", "build", "Changes that affect the build system or external dependencies"), new ConventionalCommitType("Performance Improvements", "pref", "Improves performance"),
new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"), new ConventionalCommitType("Builds", "build", "Changes that affect the build system or external dependencies"),
new("Documentations", "docs", "Updating documentation"), new ConventionalCommitType("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"),
new("Styles", "style", "Elements or code styles without changing the code logic"), new ConventionalCommitType("Documentations", "docs", "Updating documentation"),
new("Tests", "test", "Adding or updating tests"), new ConventionalCommitType("Styles", "style", "Elements or code styles without changing the code logic"),
new("Chores", "chore", "Other changes that don't modify src or test files"), new ConventionalCommitType("Tests", "test", "Adding or updating tests"),
]; new ConventionalCommitType("Chores", "chore", "Other changes that don't modify src or test files"),
};
public ConventionalCommitType(string name, string type, string description) public ConventionalCommitType(string name, string type, string description)
{ {

View file

@ -1,19 +0,0 @@
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
}
}
}

View file

@ -25,7 +25,7 @@ namespace SourceGit.Models
set; set;
} = 0; } = 0;
public static DateTimeFormat Active public static DateTimeFormat Actived
{ {
get => Supported[ActiveIndex]; get => Supported[ActiveIndex];
} }

View file

@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace SourceGit.Models
{
public class DealWithChangesAfterStashing
{
public string Label { get; set; }
public string Desc { get; set; }
public static readonly List<DealWithChangesAfterStashing> 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;
}
}
}

View file

@ -5,15 +5,6 @@ namespace SourceGit.Models
{ {
public class DiffOption public class DiffOption
{ {
/// <summary>
/// Enable `--ignore-cr-at-eol` by default?
/// </summary>
public static bool IgnoreCRAtEOL
{
get;
set;
} = true;
public Change WorkingCopyChange => _workingCopyChange; public Change WorkingCopyChange => _workingCopyChange;
public bool IsUnstaged => _isUnstaged; public bool IsUnstaged => _isUnstaged;
public List<string> Revisions => _revisions; public List<string> Revisions => _revisions;
@ -49,7 +40,7 @@ namespace SourceGit.Models
else else
{ {
if (change.DataForAmend != null) if (change.DataForAmend != null)
_extra = $"--cached {change.DataForAmend.ParentSHA}"; _extra = "--cached HEAD^";
else else
_extra = "--cached"; _extra = "--cached";
@ -65,7 +56,7 @@ namespace SourceGit.Models
/// <param name="change"></param> /// <param name="change"></param>
public DiffOption(Commit commit, Change change) public DiffOption(Commit commit, Change change)
{ {
var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
_revisions.Add(baseRevision); _revisions.Add(baseRevision);
_revisions.Add(commit.SHA); _revisions.Add(commit.SHA);
_path = change.Path; _path = change.Path;
@ -79,7 +70,7 @@ namespace SourceGit.Models
/// <param name="file"></param> /// <param name="file"></param>
public DiffOption(Commit commit, string file) public DiffOption(Commit commit, string file)
{ {
var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
_revisions.Add(baseRevision); _revisions.Add(baseRevision);
_revisions.Add(commit.SHA); _revisions.Add(commit.SHA);
_path = file; _path = file;
@ -124,6 +115,6 @@ namespace SourceGit.Models
private readonly string _path; private readonly string _path;
private readonly string _orgPath = string.Empty; private readonly string _orgPath = string.Empty;
private readonly string _extra = string.Empty; private readonly string _extra = string.Empty;
private readonly List<string> _revisions = []; private readonly List<string> _revisions = new List<string>();
} }
} }

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -16,10 +16,11 @@ namespace SourceGit.Models
Deleted, Deleted,
} }
public class TextInlineRange(int p, int n) public class TextInlineRange
{ {
public int Start { get; set; } = p; public int Start { get; set; }
public int End { get; set; } = p + n - 1; public int End { get; set; }
public TextInlineRange(int p, int n) { Start = p; End = p + n - 1; }
} }
public class TextDiffLine public class TextDiffLine
@ -146,7 +147,7 @@ namespace SourceGit.Models
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
{ {
var isTracked = !string.IsNullOrEmpty(fileBlobGuid); var isTracked = !string.IsNullOrEmpty(fileBlobGuid);
var fileGuid = isTracked ? fileBlobGuid : "00000000"; var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000";
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n'); builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
@ -555,7 +556,17 @@ namespace SourceGit.Models
} }
else if (test.Type == TextDiffLineType.Added) else if (test.Type == TextDiffLineType.Added)
{ {
if (i < start - 1 || isOldSide) if (i < start - 1)
{
if (revert)
{
newCount++;
oldCount++;
}
}
else
{
if (isOldSide)
{ {
if (revert) if (revert)
{ {
@ -567,6 +578,7 @@ namespace SourceGit.Models
{ {
newCount++; newCount++;
} }
}
if (i == end - 1 && tailed) if (i == end - 1 && tailed)
{ {
@ -643,7 +655,9 @@ namespace SourceGit.Models
public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0"; public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0";
} }
public class NoOrEOLChange; public class NoOrEOLChange
{
}
public class FileModeDiff public class FileModeDiff
{ {

View file

@ -1,12 +0,0 @@
using System;
namespace SourceGit.Models
{
[Flags]
public enum DirtyState
{
None = 0,
HasLocalChanges = 1 << 0,
HasPendingPullOrPush = 1 << 1,
}
}

View file

@ -43,7 +43,6 @@ namespace SourceGit.Models
new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(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(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()) else if (OperatingSystem.IsMacOS())

View file

@ -107,7 +107,8 @@ namespace SourceGit.Models
// Ignore // Ignore
} }
_customPaths ??= new ExternalToolPaths(); if (_customPaths == null)
_customPaths = new ExternalToolPaths();
} }
public void TryAdd(string name, string icon, Func<string> finder, Func<string, string> execArgsGenerator = null) public void TryAdd(string name, string icon, Func<string> finder, Func<string, string> execArgsGenerator = null)

View file

@ -1,46 +0,0 @@
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;
}
}
}
}

View file

@ -5,16 +5,26 @@
/// <summary> /// <summary>
/// The minimal version of Git that required by this app. /// The minimal version of Git that required by this app.
/// </summary> /// </summary>
public static readonly System.Version MINIMAL = new(2, 25, 1); public static readonly System.Version MINIMAL = new System.Version(2, 23, 0);
/// <summary>
/// The minimal version of Git that supports the `add` command with the `--pathspec-from-file` option.
/// </summary>
public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0);
/// <summary> /// <summary>
/// The minimal version of Git that supports the `stash push` command with the `--pathspec-from-file` option. /// The minimal version of Git that supports the `stash push` command with the `--pathspec-from-file` option.
/// </summary> /// </summary>
public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new(2, 26, 0); public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new System.Version(2, 26, 0);
/// <summary> /// <summary>
/// The minimal version of Git that supports the `stash push` command with the `--staged` option. /// The minimal version of Git that supports the `stash push` command with the `--staged` option.
/// </summary> /// </summary>
public static readonly System.Version STASH_PUSH_ONLY_STAGED = new(2, 35, 0); public static readonly System.Version STASH_PUSH_ONLY_STAGED = new System.Version(2, 35, 0);
/// <summary>
/// The minimal version of Git that supports the `stash show` command with the `-u` option.
/// </summary>
public static readonly System.Version STASH_SHOW_WITH_UNTRACKED = new System.Version(2, 32, 0);
} }
} }

29
src/Models/Hyperlink.cs Normal file
View file

@ -0,0 +1,29 @@
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;
}
}
}

View file

@ -2,8 +2,6 @@
{ {
public interface IRepository public interface IRepository
{ {
bool MayHaveSubmodules();
void RefreshBranches(); void RefreshBranches();
void RefreshWorktrees(); void RefreshWorktrees();
void RefreshTags(); void RefreshTags();

View file

@ -1,10 +0,0 @@
namespace SourceGit.Models
{
public enum ImageDecoder
{
None = 0,
Builtin,
Pfim,
Tiff,
}
}

View file

@ -1,37 +0,0 @@
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;
}
}
}

View file

@ -1,38 +0,0 @@
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<InlineElement> _implementation = [];
}
}

View file

@ -8,7 +8,10 @@ namespace SourceGit.Models
{ {
public class IpcChannel : IDisposable public class IpcChannel : IDisposable
{ {
public bool IsFirstInstance { get; } public bool IsFirstInstance
{
get => _isFirstInstance;
}
public event Action<string> MessageReceived; public event Action<string> MessageReceived;
@ -16,10 +19,10 @@ namespace SourceGit.Models
{ {
try try
{ {
_singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); _singletoneLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
IsFirstInstance = true; _isFirstInstance = true;
_server = new NamedPipeServerStream( _server = new NamedPipeServerStream(
"SourceGitIPCChannel" + Environment.UserName, "SourceGitIPCChannel",
PipeDirection.In, PipeDirection.In,
-1, -1,
PipeTransmissionMode.Byte, PipeTransmissionMode.Byte,
@ -29,7 +32,7 @@ namespace SourceGit.Models
} }
catch catch
{ {
IsFirstInstance = false; _isFirstInstance = false;
} }
} }
@ -37,7 +40,7 @@ namespace SourceGit.Models
{ {
try try
{ {
using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel", PipeDirection.Out))
{ {
client.Connect(1000); client.Connect(1000);
if (!client.IsConnected) if (!client.IsConnected)
@ -64,7 +67,7 @@ namespace SourceGit.Models
public void Dispose() public void Dispose()
{ {
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();
_singletonLock?.Dispose(); _singletoneLock?.Dispose();
} }
private async void StartServer() private async void StartServer()
@ -93,7 +96,8 @@ namespace SourceGit.Models
} }
} }
private FileStream _singletonLock = null; private FileStream _singletoneLock = null;
private bool _isFirstInstance = false;
private NamedPipeServerStream _server = null; private NamedPipeServerStream _server = null;
private CancellationTokenSource _cancellationTokenSource = null; private CancellationTokenSource _cancellationTokenSource = null;
} }

View file

@ -1,4 +1,5 @@
using System.Text.RegularExpressions; using System.Collections.Generic;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -45,7 +46,7 @@ namespace SourceGit.Models
set => SetProperty(ref _urlTemplate, value); set => SetProperty(ref _urlTemplate, value);
} }
public void Matches(InlineElementCollector outs, string message) public void Matches(List<Hyperlink> outs, string message)
{ {
if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return; return;
@ -59,7 +60,17 @@ namespace SourceGit.Models
var start = match.Index; var start = match.Index;
var len = match.Length; var len = match.Length;
if (outs.Intersect(start, len) != null) var intersect = false;
foreach (var exist in outs)
{
if (exist.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue; continue;
var link = _urlTemplate; var link = _urlTemplate;
@ -70,7 +81,8 @@ namespace SourceGit.Models
link = link.Replace($"${j}", group.Value); link = link.Replace($"${j}", group.Value);
} }
outs.Add(new InlineElement(InlineElementType.Link, start, len, link)); var range = new Hyperlink(start, len, link);
outs.Add(range);
} }
} }

View file

@ -1,22 +1,8 @@
using System.Text.RegularExpressions; namespace SourceGit.Models
namespace SourceGit.Models
{ {
public partial class LFSObject public 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 string Oid { get; set; } = string.Empty;
public long Size { get; set; } = 0; 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;
}
} }
} }

View file

@ -1,4 +1,6 @@
namespace SourceGit.Models namespace SourceGit.Models
{ {
public class Null; public class Null
{
}
} }

View file

@ -1,6 +1,4 @@
using System; namespace SourceGit.Models
namespace SourceGit.Models
{ {
public static class NumericSort public static class NumericSort
{ {
@ -12,35 +10,52 @@ namespace SourceGit.Models
int marker1 = 0; int marker1 = 0;
int marker2 = 0; int marker2 = 0;
char[] tmp1 = new char[len1];
char[] tmp2 = new char[len2];
while (marker1 < len1 && marker2 < len2) while (marker1 < len1 && marker2 < len2)
{ {
char c1 = s1[marker1]; char c1 = s1[marker1];
char c2 = s2[marker2]; char c2 = s2[marker2];
int loc1 = 0;
int loc2 = 0;
bool isDigit1 = char.IsDigit(c1); bool isDigit1 = char.IsDigit(c1);
bool isDigit2 = char.IsDigit(c2); bool isDigit2 = char.IsDigit(c2);
if (isDigit1 != isDigit2) if (isDigit1 != isDigit2)
return c1.CompareTo(c2); return c1.CompareTo(c2);
int subLen1 = 1; do
while (marker1 + subLen1 < len1 && char.IsDigit(s1[marker1 + subLen1]) == isDigit1) {
subLen1++; tmp1[loc1] = c1;
loc1++;
marker1++;
int subLen2 = 1; if (marker1 < len1)
while (marker2 + subLen2 < len2 && char.IsDigit(s2[marker2 + subLen2]) == isDigit2) c1 = s1[marker1];
subLen2++; else
break;
} while (char.IsDigit(c1) == isDigit1);
string sub1 = s1.Substring(marker1, subLen1); do
string sub2 = s2.Substring(marker2, subLen2); {
tmp2[loc2] = c2;
loc2++;
marker2++;
marker1 += subLen1; if (marker2 < len2)
marker2 += subLen2; c2 = s2[marker2];
else
break;
} while (char.IsDigit(c2) == isDigit2);
string sub1 = new string(tmp1, 0, loc1);
string sub2 = new string(tmp2, 0, loc2);
int result; int result;
if (isDigit1) if (isDigit1)
result = (subLen1 == subLen2) ? string.CompareOrdinal(sub1, sub2) : (subLen1 - subLen2); result = loc1 == loc2 ? string.CompareOrdinal(sub1, sub2) : loc1 - loc2;
else else
result = string.Compare(sub1, sub2, StringComparison.OrdinalIgnoreCase); result = string.CompareOrdinal(sub1, sub2);
if (result != 0) if (result != 0)
return result; return result;

View file

@ -6,10 +6,8 @@ namespace SourceGit.Models
{ {
public partial class Remote public partial class Remote
{ {
[GeneratedRegex(@"^https?://[^/]+/.+[^/\.]$")] [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)?$")]
private static partial Regex REG_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)?$")] [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
private static partial Regex REG_SSH1(); private static partial Regex REG_SSH1();
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")] [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
@ -20,7 +18,6 @@ namespace SourceGit.Models
private static readonly Regex[] URL_FORMATS = [ private static readonly Regex[] URL_FORMATS = [
REG_HTTPS(), REG_HTTPS(),
REG_GIT(),
REG_SSH1(), REG_SSH1(),
REG_SSH2(), REG_SSH2(),
]; ];
@ -33,10 +30,13 @@ namespace SourceGit.Models
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
return false; return false;
if (REG_SSH1().IsMatch(url)) for (int i = 1; i < URL_FORMATS.Length; i++)
{
if (URL_FORMATS[i].IsMatch(url))
return true; return true;
}
return REG_SSH2().IsMatch(url); return false;
} }
public static bool IsValidURL(string url) public static bool IsValidURL(string url)
@ -50,10 +50,7 @@ namespace SourceGit.Models
return true; return true;
} }
return url.StartsWith("file://", StringComparison.Ordinal) || return url.EndsWith(".git", StringComparison.Ordinal) && Directory.Exists(url);
url.StartsWith("./", StringComparison.Ordinal) ||
url.StartsWith("../", StringComparison.Ordinal) ||
Directory.Exists(url);
} }
public bool TryGetVisitURL(out string url) public bool TryGetVisitURL(out string url)

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@ -32,24 +32,12 @@ namespace SourceGit.Models
set; set;
} = false; } = false;
public bool OnlyHighlightCurrentBranchInHistories public bool OnlyHighlighCurrentBranchInHistories
{ {
get; get;
set; set;
} = false; } = false;
public BranchSortMode LocalBranchSortMode
{
get;
set;
} = BranchSortMode.Name;
public BranchSortMode RemoteBranchSortMode
{
get;
set;
} = BranchSortMode.Name;
public TagSortMode TagSortMode public TagSortMode TagSortMode
{ {
get; get;
@ -80,6 +68,18 @@ namespace SourceGit.Models
set; set;
} = true; } = true;
public bool FetchWithoutTagsOnPull
{
get;
set;
} = false;
public bool FetchAllBranchesOnPull
{
get;
set;
} = true;
public bool CheckSubmodulesOnPush public bool CheckSubmodulesOnPush
{ {
get; get;
@ -110,12 +110,6 @@ namespace SourceGit.Models
set; set;
} = true; } = true;
public bool UpdateSubmodulesOnCheckoutBranch
{
get;
set;
} = true;
public AvaloniaList<Filter> HistoriesFilters public AvaloniaList<Filter> HistoriesFilters
{ {
get; get;
@ -176,13 +170,19 @@ namespace SourceGit.Models
set; set;
} = false; } = false;
public int ChangesAfterStashing public bool KeepIndexWhenStash
{ {
get; get;
set; set;
} = 0; } = false;
public string PreferredOpenAIService public bool AutoRestoreAfterStash
{
get;
set;
} = false;
public string PreferedOpenAIService
{ {
get; get;
set; set;
@ -281,8 +281,9 @@ namespace SourceGit.Models
return false; return false;
} }
foreach (var filter in HistoriesFilters) for (int i = 0; i < HistoriesFilters.Count; i++)
{ {
var filter = HistoriesFilters[i];
if (filter.Type != type) if (filter.Type != type)
continue; continue;
@ -325,28 +326,28 @@ namespace SourceGit.Models
if (filter.Mode == FilterMode.Included) if (filter.Mode == FilterMode.Included)
includedRefs.Add(filter.Pattern); includedRefs.Add(filter.Pattern);
else if (filter.Mode == FilterMode.Excluded) else if (filter.Mode == FilterMode.Excluded)
excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\"");
} }
else if (filter.Type == FilterType.LocalBranchFolder) else if (filter.Type == FilterType.LocalBranchFolder)
{ {
if (filter.Mode == FilterMode.Included) if (filter.Mode == FilterMode.Included)
includedRefs.Add($"--branches={filter.Pattern.AsSpan(11)}/*"); includedRefs.Add($"--branches={filter.Pattern.Substring(11)}/*");
else if (filter.Mode == FilterMode.Excluded) else if (filter.Mode == FilterMode.Excluded)
excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\"");
} }
else if (filter.Type == FilterType.RemoteBranch) else if (filter.Type == FilterType.RemoteBranch)
{ {
if (filter.Mode == FilterMode.Included) if (filter.Mode == FilterMode.Included)
includedRefs.Add(filter.Pattern); includedRefs.Add(filter.Pattern);
else if (filter.Mode == FilterMode.Excluded) else if (filter.Mode == FilterMode.Excluded)
excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); excludedRemotes.Add($"--exclude=\"{filter.Pattern.Substring(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\"");
} }
else if (filter.Type == FilterType.RemoteBranchFolder) else if (filter.Type == FilterType.RemoteBranchFolder)
{ {
if (filter.Mode == FilterMode.Included) if (filter.Mode == FilterMode.Included)
includedRefs.Add($"--remotes={filter.Pattern.AsSpan(13)}/*"); includedRefs.Add($"--remotes={filter.Pattern.Substring(13)}/*");
else if (filter.Mode == FilterMode.Excluded) else if (filter.Mode == FilterMode.Excluded)
excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); excludedRemotes.Add($"--exclude=\"{filter.Pattern.Substring(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\"");
} }
else if (filter.Type == FilterType.Tag) else if (filter.Type == FilterType.Tag)
{ {
@ -398,7 +399,6 @@ namespace SourceGit.Models
public void PushCommitMessage(string message) public void PushCommitMessage(string message)
{ {
message = message.Trim().ReplaceLineEndings("\n");
var existIdx = CommitMessages.IndexOf(message); var existIdx = CommitMessages.IndexOf(message);
if (existIdx == 0) if (existIdx == 0)
return; return;
@ -446,19 +446,5 @@ namespace SourceGit.Models
if (act != null) if (act != null)
CustomActions.Remove(act); 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);
}
} }
} }

View file

@ -1,6 +1,4 @@
using System.Globalization; using Avalonia.Media.Imaging;
using System.IO;
using Avalonia.Media.Imaging;
namespace SourceGit.Models namespace SourceGit.Models
{ {
@ -11,17 +9,10 @@ namespace SourceGit.Models
public class RevisionImageFile public class RevisionImageFile
{ {
public Bitmap Image { get; } public Bitmap Image { get; set; } = null;
public long FileSize { get; } public long FileSize { get; set; } = 0;
public string ImageType { get; } public string ImageType { get; set; } = string.Empty;
public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0"; 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 public class RevisionTextFile

View file

@ -33,7 +33,9 @@ namespace SourceGit.Models
} }
} }
public class AlreadyUpToDate; public class AlreadyUpToDate
{
}
public class SelfUpdateFailed public class SelfUpdateFailed
{ {

View file

@ -42,8 +42,7 @@ namespace SourceGit.Models
new ShellOrTerminal("mac-terminal", "Terminal", ""), new ShellOrTerminal("mac-terminal", "Terminal", ""),
new ShellOrTerminal("iterm2", "iTerm", ""), new ShellOrTerminal("iterm2", "iTerm", ""),
new ShellOrTerminal("warp", "Warp", ""), new ShellOrTerminal("warp", "Warp", ""),
new ShellOrTerminal("ghostty", "Ghostty", ""), new ShellOrTerminal("ghostty", "Ghostty", "")
new ShellOrTerminal("kitty", "kitty", "")
}; };
} }
else else
@ -59,7 +58,6 @@ namespace SourceGit.Models
new ShellOrTerminal("foot", "Foot", "foot"), new ShellOrTerminal("foot", "Foot", "foot"),
new ShellOrTerminal("wezterm", "WezTerm", "wezterm"), new ShellOrTerminal("wezterm", "WezTerm", "wezterm"),
new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"), new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"),
new ShellOrTerminal("kitty", "kitty", "kitty"),
new ShellOrTerminal("custom", "Custom", ""), new ShellOrTerminal("custom", "Custom", ""),
}; };
} }

View file

@ -11,24 +11,6 @@ namespace SourceGit.Models
public ulong Time { get; set; } = 0; public ulong Time { get; set; } = 0;
public string Message { get; set; } = ""; public string Message { get; set; } = "";
public string Subject public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
{
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);
}
}
} }
} }

View file

@ -26,23 +26,25 @@ namespace SourceGit.Models
public class StatisticsReport public class StatisticsReport
{ {
public static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
public int Total { get; set; } = 0; public int Total { get; set; } = 0;
public List<StatisticsAuthor> Authors { get; set; } = new(); public List<StatisticsAuthor> Authors { get; set; } = new List<StatisticsAuthor>();
public List<ISeries> Series { get; set; } = new(); public List<ISeries> Series { get; set; } = new List<ISeries>();
public List<Axis> XAxes { get; set; } = new(); public List<Axis> XAxes { get; set; } = new List<Axis>();
public List<Axis> YAxes { get; set; } = new(); public List<Axis> YAxes { get; set; } = new List<Axis>();
public StatisticsAuthor SelectedAuthor { get => _selectedAuthor; set => ChangeAuthor(value); } public StatisticsAuthor SelectedAuthor { get => _selectedAuthor; set => ChangeAuthor(value); }
public StatisticsReport(StatisticsMode mode, DateTime start) public StatisticsReport(StatisticsMode mode, DateTime start)
{ {
_mode = mode; _mode = mode;
YAxes.Add(new Axis() YAxes = [new Axis()
{ {
TextSize = 10, TextSize = 10,
MinLimit = 0, MinLimit = 0,
SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 } SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 }
}); }];
if (mode == StatisticsMode.ThisWeek) if (mode == StatisticsMode.ThisWeek)
{ {
@ -70,7 +72,7 @@ namespace SourceGit.Models
{ {
Total++; Total++;
DateTime normalized; var normalized = DateTime.MinValue;
if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth) if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth)
normalized = time.Date; normalized = time.Date;
else else
@ -170,27 +172,26 @@ namespace SourceGit.Models
ChangeColor(_fillColor); ChangeColor(_fillColor);
} }
private static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; private StatisticsMode _mode = StatisticsMode.All;
private StatisticsMode _mode; private Dictionary<User, int> _mapUsers = new Dictionary<User, int>();
private Dictionary<User, int> _mapUsers = new(); private Dictionary<DateTime, int> _mapSamples = new Dictionary<DateTime, int>();
private Dictionary<DateTime, int> _mapSamples = new(); private Dictionary<User, Dictionary<DateTime, int>> _mapUserSamples = new Dictionary<User, Dictionary<DateTime, int>>();
private Dictionary<User, Dictionary<DateTime, int>> _mapUserSamples = new();
private StatisticsAuthor _selectedAuthor = null; private StatisticsAuthor _selectedAuthor = null;
private uint _fillColor = 255; private uint _fillColor = 255;
} }
public class Statistics public class Statistics
{ {
public StatisticsReport All { get; } public StatisticsReport All { get; set; }
public StatisticsReport Month { get; } public StatisticsReport Month { get; set; }
public StatisticsReport Week { get; } public StatisticsReport Week { get; set; }
public Statistics() public Statistics()
{ {
var today = DateTime.Now.ToLocalTime().Date; _today = DateTime.Now.ToLocalTime().Date;
var weekOffset = (7 + (int)today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7; var weekOffset = (7 + (int)_today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7;
_thisWeekStart = today.AddDays(-weekOffset); _thisWeekStart = _today.AddDays(-weekOffset);
_thisMonthStart = today.AddDays(1 - today.Day); _thisMonthStart = _today.AddDays(1 - _today.Day);
All = new StatisticsReport(StatisticsMode.All, DateTime.MinValue); All = new StatisticsReport(StatisticsMode.All, DateTime.MinValue);
Month = new StatisticsReport(StatisticsMode.ThisMonth, _thisMonthStart); Month = new StatisticsReport(StatisticsMode.ThisMonth, _thisMonthStart);
@ -199,13 +200,7 @@ namespace SourceGit.Models
public void AddCommit(string author, double timestamp) public void AddCommit(string author, double timestamp)
{ {
var emailIdx = author.IndexOf('±', StringComparison.Ordinal); var user = User.FindOrAdd(author);
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(); var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
if (time >= _thisWeekStart) if (time >= _thisWeekStart)
@ -219,15 +214,13 @@ namespace SourceGit.Models
public void Complete() public void Complete()
{ {
_users.Clear();
All.Complete(); All.Complete();
Month.Complete(); Month.Complete();
Week.Complete(); Week.Complete();
} }
private readonly DateTime _today;
private readonly DateTime _thisMonthStart; private readonly DateTime _thisMonthStart;
private readonly DateTime _thisWeekStart; private readonly DateTime _thisWeekStart;
private readonly Dictionary<string, User> _users = new();
} }
} }

View file

@ -1,20 +1,8 @@
namespace SourceGit.Models namespace SourceGit.Models
{ {
public enum SubmoduleStatus
{
Normal = 0,
NotInited,
RevisionChanged,
Unmerged,
Modified,
}
public class Submodule public class Submodule
{ {
public string Path { get; set; } = string.Empty; public string Path { get; set; } = "";
public string SHA { get; set; } = string.Empty; public bool IsDirty { get; set; } = false;
public string URL { get; set; } = string.Empty;
public SubmoduleStatus Status { get; set; } = SubmoduleStatus.Normal;
public bool IsDirty => Status > SubmoduleStatus.NotInited;
} }
} }

View file

@ -5,13 +5,13 @@ namespace SourceGit.Models
public enum TagSortMode public enum TagSortMode
{ {
CreatorDate = 0, CreatorDate = 0,
Name, NameInAscending,
NameInDescending,
} }
public class Tag : ObservableObject public class Tag : ObservableObject
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public bool IsAnnotated { get; set; } = false;
public string SHA { get; set; } = string.Empty; public string SHA { get; set; } = string.Empty;
public ulong CreatorDate { get; set; } = 0; public ulong CreatorDate { get; set; } = 0;
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;

View file

@ -102,7 +102,7 @@ namespace SourceGit.Models
private int? Integer() private int? Integer()
{ {
var start = _pos; var start = _pos;
while (Peek() is >= '0' and <= '9') while (Peek() is char c && c >= '0' && c <= '9')
{ {
_pos++; _pos++;
} }
@ -118,7 +118,7 @@ namespace SourceGit.Models
// text token start // text token start
var tok = _pos; var tok = _pos;
bool esc = false; bool esc = false;
while (Next() is { } c) while (Next() is char c)
{ {
if (esc) if (esc)
{ {
@ -129,7 +129,7 @@ namespace SourceGit.Models
{ {
case ESCAPE: case ESCAPE:
// allow to escape only \ and $ // allow to escape only \ and $
if (Peek() is { } nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR)) if (Peek() is char nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR))
{ {
esc = true; esc = true;
FlushText(tok, _pos - 1); FlushText(tok, _pos - 1);
@ -173,7 +173,7 @@ namespace SourceGit.Models
if (Next() != VARIABLE_START) if (Next() != VARIABLE_START)
return null; return null;
int name_start = _pos; int name_start = _pos;
while (Next() is { } c) while (Next() is char c)
{ {
// name character, continue advancing // name character, continue advancing
if (IsNameChar(c)) if (IsNameChar(c))
@ -228,7 +228,7 @@ namespace SourceGit.Models
var sb = new StringBuilder(); var sb = new StringBuilder();
var tok = _pos; var tok = _pos;
var esc = false; var esc = false;
while (Next() is { } c) while (Next() is char c)
{ {
if (esc) if (esc)
{ {
@ -277,7 +277,7 @@ namespace SourceGit.Models
var sb = new StringBuilder(); var sb = new StringBuilder();
var tok = _pos; var tok = _pos;
var esc = false; var esc = false;
while (Next() is { } c) while (Next() is char c)
{ {
if (esc) if (esc)
{ {
@ -402,7 +402,7 @@ namespace SourceGit.Models
sb.AppendJoin(", ", paths); sb.AppendJoin(", ", paths);
if (max < context.changes.Count) if (max < context.changes.Count)
sb.Append($" and {context.changes.Count - max} other files"); sb.AppendFormat(" and {0} other files", context.changes.Count - max);
return sb.ToString(); return sb.ToString();
} }

View file

@ -2,22 +2,30 @@
namespace SourceGit.Models namespace SourceGit.Models
{ {
public class TextInlineChange(int dp, int dc, int ap, int ac) public class TextInlineChange
{ {
public int DeletedStart { get; set; } = dp; public int DeletedStart { get; set; }
public int DeletedCount { get; set; } = dc; public int DeletedCount { get; set; }
public int AddedStart { get; set; } = ap; public int AddedStart { get; set; }
public int AddedCount { get; set; } = ac; public int AddedCount { get; set; }
private class Chunk(int hash, int start, int size) class Chunk
{ {
public readonly int Hash = hash; public int Hash;
public readonly int Start = start;
public readonly int Size = size;
public bool Modified; 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;
}
} }
private enum Edit enum Edit
{ {
None, None,
DeletedRight, DeletedRight,
@ -26,7 +34,7 @@ namespace SourceGit.Models
AddedLeft, AddedLeft,
} }
private class EditResult class EditResult
{ {
public Edit State; public Edit State;
public int DeleteStart; public int DeleteStart;
@ -35,6 +43,14 @@ namespace SourceGit.Models
public int AddEnd; public int AddEnd;
} }
public TextInlineChange(int dp, int dc, int ap, int ac)
{
DeletedStart = dp;
DeletedCount = dc;
AddedStart = ap;
AddedCount = ac;
}
public static List<TextInlineChange> Compare(string oldValue, string newValue) public static List<TextInlineChange> Compare(string oldValue, string newValue)
{ {
var hashes = new Dictionary<string, int>(); var hashes = new Dictionary<string, int>();
@ -188,10 +204,11 @@ namespace SourceGit.Models
for (int i = 0; i <= half; i++) for (int i = 0; i <= half; i++)
{ {
for (int j = -i; j <= i; j += 2) for (int j = -i; j <= i; j += 2)
{ {
var idx = j + half; var idx = j + half;
int o; int o, n;
if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1])) if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1]))
{ {
o = forward[idx + 1]; o = forward[idx + 1];
@ -203,7 +220,7 @@ namespace SourceGit.Models
rs.State = Edit.DeletedRight; rs.State = Edit.DeletedRight;
} }
var n = o - j; n = o - j;
var startX = o; var startX = o;
var startY = n; var startY = n;
@ -241,7 +258,7 @@ namespace SourceGit.Models
for (int j = -i; j <= i; j += 2) for (int j = -i; j <= i; j += 2)
{ {
var idx = j + half; var idx = j + half;
int o; int o, n;
if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1])) if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1]))
{ {
o = reverse[idx + 1] - 1; o = reverse[idx + 1] - 1;
@ -253,7 +270,7 @@ namespace SourceGit.Models
rs.State = Edit.AddedLeft; rs.State = Edit.AddedLeft;
} }
var n = o - (j + delta); n = o - (j + delta);
var endX = o; var endX = o;
var endY = n; var endY = n;
@ -295,7 +312,8 @@ namespace SourceGit.Models
private static void AddChunk(List<Chunk> chunks, Dictionary<string, int> hashes, string data, int start) private static void AddChunk(List<Chunk> chunks, Dictionary<string, int> hashes, string data, int start)
{ {
if (hashes.TryGetValue(data, out var hash)) int hash;
if (hashes.TryGetValue(data, out hash))
{ {
chunks.Add(new Chunk(hash, start, data.Length)); chunks.Add(new Chunk(hash, start, data.Length));
} }

View file

@ -26,7 +26,11 @@ namespace SourceGit.Models
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
return obj is User other && Name == other.Name && Email == other.Email; if (obj == null || !(obj is User))
return false;
var other = obj as User;
return Name == other.Name && Email == other.Email;
} }
public override int GetHashCode() public override int GetHashCode()

View file

@ -12,50 +12,27 @@ namespace SourceGit.Models
{ {
_repo = repo; _repo = repo;
var testGitDir = new DirectoryInfo(Path.Combine(fullpath, ".git")).FullName; _wcWatcher = new FileSystemWatcher();
var desiredDir = new DirectoryInfo(gitDir).FullName; _wcWatcher.Path = fullpath;
if (testGitDir.Equals(desiredDir, StringComparison.Ordinal)) _wcWatcher.Filter = "*";
{ _wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime;
var combined = new FileSystemWatcher(); _wcWatcher.IncludeSubdirectories = true;
combined.Path = fullpath; _wcWatcher.Created += OnWorkingCopyChanged;
combined.Filter = "*"; _wcWatcher.Renamed += OnWorkingCopyChanged;
combined.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; _wcWatcher.Changed += OnWorkingCopyChanged;
combined.IncludeSubdirectories = true; _wcWatcher.Deleted += OnWorkingCopyChanged;
combined.Created += OnRepositoryChanged; _wcWatcher.EnableRaisingEvents = true;
combined.Renamed += OnRepositoryChanged;
combined.Changed += OnRepositoryChanged;
combined.Deleted += OnRepositoryChanged;
combined.EnableRaisingEvents = true;
_watchers.Add(combined); _repoWatcher = new FileSystemWatcher();
} _repoWatcher.Path = gitDir;
else _repoWatcher.Filter = "*";
{ _repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;
var wc = new FileSystemWatcher(); _repoWatcher.IncludeSubdirectories = true;
wc.Path = fullpath; _repoWatcher.Created += OnRepositoryChanged;
wc.Filter = "*"; _repoWatcher.Renamed += OnRepositoryChanged;
wc.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; _repoWatcher.Changed += OnRepositoryChanged;
wc.IncludeSubdirectories = true; _repoWatcher.Deleted += OnRepositoryChanged;
wc.Created += OnWorkingCopyChanged; _repoWatcher.EnableRaisingEvents = true;
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); _timer = new Timer(Tick, null, 100, 100);
} }
@ -100,13 +77,22 @@ namespace SourceGit.Models
public void Dispose() public void Dispose()
{ {
foreach (var watcher in _watchers) _repoWatcher.EnableRaisingEvents = false;
{ _repoWatcher.Created -= OnRepositoryChanged;
watcher.EnableRaisingEvents = false; _repoWatcher.Renamed -= OnRepositoryChanged;
watcher.Dispose(); _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;
_watchers.Clear();
_timer.Dispose(); _timer.Dispose();
_timer = null; _timer = null;
} }
@ -121,6 +107,7 @@ namespace SourceGit.Models
{ {
_updateBranch = 0; _updateBranch = 0;
_updateWC = 0; _updateWC = 0;
_updateSubmodules = 0;
if (_updateTags > 0) if (_updateTags > 0)
{ {
@ -128,15 +115,10 @@ namespace SourceGit.Models
Task.Run(_repo.RefreshTags); Task.Run(_repo.RefreshTags);
} }
if (_updateSubmodules > 0 || _repo.MayHaveSubmodules())
{
_updateSubmodules = 0;
Task.Run(_repo.RefreshSubmodules);
}
Task.Run(_repo.RefreshBranches); Task.Run(_repo.RefreshBranches);
Task.Run(_repo.RefreshCommits); Task.Run(_repo.RefreshCommits);
Task.Run(_repo.RefreshWorkingCopyChanges); Task.Run(_repo.RefreshWorkingCopyChanges);
Task.Run(_repo.RefreshSubmodules);
Task.Run(_repo.RefreshWorktrees); Task.Run(_repo.RefreshWorktrees);
} }
@ -168,64 +150,15 @@ namespace SourceGit.Models
private void OnRepositoryChanged(object o, FileSystemEventArgs e) private void OnRepositoryChanged(object o, FileSystemEventArgs e)
{ {
if (string.IsNullOrEmpty(e.Name) || e.Name.Equals(".git", StringComparison.Ordinal)) if (string.IsNullOrEmpty(e.Name) || e.Name.EndsWith(".lock", StringComparison.Ordinal))
return; return;
var name = e.Name.Replace('\\', '/').TrimEnd('/'); var name = e.Name.Replace("\\", "/");
if (name.EndsWith("/.git", StringComparison.Ordinal)) if (name.StartsWith("modules", StringComparison.Ordinal) && name.EndsWith("HEAD", 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))
{
if (name.EndsWith("/HEAD", StringComparison.Ordinal) ||
name.EndsWith("/ORIG_HEAD", StringComparison.Ordinal))
{ {
_updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
_updateWC = 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)) else if (name.StartsWith("refs/tags", StringComparison.Ordinal))
{ {
_updateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); _updateTags = DateTime.Now.AddSeconds(.5).ToFileTime();
@ -235,7 +168,6 @@ namespace SourceGit.Models
_updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); _updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime();
} }
else if (name.Equals("HEAD", StringComparison.Ordinal) || else if (name.Equals("HEAD", StringComparison.Ordinal) ||
name.Equals("BISECT_START", StringComparison.Ordinal) ||
name.StartsWith("refs/heads/", StringComparison.Ordinal) || name.StartsWith("refs/heads/", StringComparison.Ordinal) ||
name.StartsWith("refs/remotes/", StringComparison.Ordinal) || name.StartsWith("refs/remotes/", StringComparison.Ordinal) ||
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
@ -248,16 +180,14 @@ namespace SourceGit.Models
} }
} }
private void HandleWorkingCopyFileChanged(string name) private void OnWorkingCopyChanged(object o, FileSystemEventArgs e)
{ {
if (name.StartsWith(".vs/", StringComparison.Ordinal)) if (string.IsNullOrEmpty(e.Name))
return; return;
if (name.Equals(".gitmodules", StringComparison.Ordinal)) var name = e.Name.Replace("\\", "/");
{ if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal))
_updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
return; return;
}
lock (_lockSubmodule) lock (_lockSubmodule)
{ {
@ -275,7 +205,8 @@ namespace SourceGit.Models
} }
private readonly IRepository _repo = null; private readonly IRepository _repo = null;
private List<FileSystemWatcher> _watchers = []; private FileSystemWatcher _repoWatcher = null;
private FileSystemWatcher _wcWatcher = null;
private Timer _timer = null; private Timer _timer = null;
private int _lockCount = 0; private int _lockCount = 0;
private long _updateWC = 0; private long _updateWC = 0;
@ -284,7 +215,7 @@ namespace SourceGit.Models
private long _updateStashes = 0; private long _updateStashes = 0;
private long _updateTags = 0; private long _updateTags = 0;
private readonly Lock _lockSubmodule = new(); private object _lockSubmodule = new object();
private List<string> _submodules = new List<string>(); private List<string> _submodules = new List<string>();
} }
} }

View file

@ -1,5 +1,4 @@
using System; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models namespace SourceGit.Models
{ {
@ -23,12 +22,12 @@ namespace SourceGit.Models
get get
{ {
if (IsDetached) if (IsDetached)
return $"detached HEAD at {Head.AsSpan(10)}"; return $"deteched HEAD at {Head.Substring(10)}";
if (Branch.StartsWith("refs/heads/", StringComparison.Ordinal)) if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
return Branch.Substring(11); return Branch.Substring(11);
if (Branch.StartsWith("refs/remotes/", StringComparison.Ordinal)) if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
return Branch.Substring(13); return Branch.Substring(13);
return Branch; return Branch;

View file

@ -5,8 +5,6 @@ using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform;
namespace SourceGit.Native namespace SourceGit.Native
{ {
@ -18,21 +16,6 @@ namespace SourceGit.Native
builder.With(new X11PlatformOptions() { EnableIme = true }); 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() public string FindGitExecutable()
{ {
return FindExecutable("git"); return FindExecutable("git");
@ -120,8 +103,8 @@ namespace SourceGit.Native
private string FindExecutable(string filename) private string FindExecutable(string filename)
{ {
var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var paths = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var path in paths) foreach (var path in pathes)
{ {
var test = Path.Combine(path, filename); var test = Path.Combine(path, filename);
if (File.Exists(test)) if (File.Exists(test))

View file

@ -5,8 +5,6 @@ using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform;
namespace SourceGit.Native namespace SourceGit.Native
{ {
@ -38,12 +36,6 @@ namespace SourceGit.Native
Environment.SetEnvironmentVariable("PATH", path); Environment.SetEnvironmentVariable("PATH", path);
} }
public void SetupWindow(Window window)
{
window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome;
window.ExtendClientAreaToDecorationsHint = true;
}
public string FindGitExecutable() public string FindGitExecutable()
{ {
var gitPathVariants = new List<string>() { var gitPathVariants = new List<string>() {
@ -67,8 +59,6 @@ namespace SourceGit.Native
return "Warp"; return "Warp";
case "ghostty": case "ghostty":
return "Ghostty"; return "Ghostty";
case "kitty":
return "kitty";
} }
return string.Empty; return string.Empty;

View file

@ -6,7 +6,6 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Avalonia; using Avalonia;
using Avalonia.Controls;
namespace SourceGit.Native namespace SourceGit.Native
{ {
@ -15,7 +14,6 @@ namespace SourceGit.Native
public interface IBackend public interface IBackend
{ {
void SetupApp(AppBuilder builder); void SetupApp(AppBuilder builder);
void SetupWindow(Window window);
string FindGitExecutable(); string FindGitExecutable();
string FindTerminal(Models.ShellOrTerminal shell); string FindTerminal(Models.ShellOrTerminal shell);
@ -70,12 +68,6 @@ namespace SourceGit.Native
set; set;
} = []; } = [];
public static bool UseSystemWindowFrame
{
get => OperatingSystem.IsLinux() && _enableSystemWindowFrame;
set => _enableSystemWindowFrame = value;
}
static OS() static OS()
{ {
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
@ -124,16 +116,11 @@ namespace SourceGit.Native
Directory.CreateDirectory(DataDir); Directory.CreateDirectory(DataDir);
} }
public static void SetupExternalTools() public static void SetupEnternalTools()
{ {
ExternalTools = _backend.FindExternalTools(); ExternalTools = _backend.FindExternalTools();
} }
public static void SetupForWindow(Window window)
{
_backend.SetupWindow(window);
}
public static string FindGitExecutable() public static string FindGitExecutable()
{ {
return _backend.FindGitExecutable(); return _backend.FindGitExecutable();
@ -238,6 +225,5 @@ namespace SourceGit.Native
private static IBackend _backend = null; private static IBackend _backend = null;
private static string _gitExecutable = string.Empty; private static string _gitExecutable = string.Empty;
private static bool _enableSystemWindowFrame = false;
} }
} }

View file

@ -8,7 +8,6 @@ using System.Text;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
namespace SourceGit.Native namespace SourceGit.Native
@ -16,12 +15,16 @@ namespace SourceGit.Native
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
internal class Windows : OS.IBackend internal class Windows : OS.IBackend
{ {
internal struct RECT [StructLayout(LayoutKind.Sequential)]
internal struct RTL_OSVERSIONINFOEX
{ {
public int left; internal uint dwOSVersionInfoSize;
public int top; internal uint dwMajorVersion;
public int right; internal uint dwMinorVersion;
public int bottom; internal uint dwBuildNumber;
internal uint dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
internal string szCSDVersion;
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@ -33,6 +36,9 @@ namespace SourceGit.Native
public int cyBottomHeight; public int cyBottomHeight;
} }
[DllImport("ntdll.dll")]
private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation);
[DllImport("dwmapi.dll")] [DllImport("dwmapi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);
@ -48,96 +54,41 @@ namespace SourceGit.Native
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)] [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags); 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) public void SetupApp(AppBuilder builder)
{ {
// Fix drop shadow issue on Windows 10 // Fix drop shadow issue on Windows 10
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 22000)) RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX();
v.dwOSVersionInfoSize = (uint)Marshal.SizeOf<RTL_OSVERSIONINFOEX>();
if (RtlGetVersion(ref v) == 0 && (v.dwMajorVersion < 10 || v.dwBuildNumber < 22000))
{ {
Window.WindowStateProperty.Changed.AddClassHandler<Window>((w, _) => FixWindowFrameOnWin10(w)); Window.WindowStateProperty.Changed.AddClassHandler<Window>((w, _) => FixWindowFrameOnWin10(w));
Control.LoadedEvent.AddClassHandler<Window>((w, _) => FixWindowFrameOnWin10(w)); Control.LoadedEvent.AddClassHandler<Window>((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() public string FindGitExecutable()
{ {
var reg = Microsoft.Win32.RegistryKey.OpenBaseKey( var reg = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryHive.LocalMachine,
Microsoft.Win32.RegistryView.Registry64); Microsoft.Win32.RegistryView.Registry64);
var git = reg.OpenSubKey(@"SOFTWARE\GitForWindows"); var git = reg.OpenSubKey("SOFTWARE\\GitForWindows");
if (git?.GetValue("InstallPath") is string installPath) if (git != null && git.GetValue("InstallPath") is string installPath)
{
return Path.Combine(installPath, "bin", "git.exe"); return Path.Combine(installPath, "bin", "git.exe");
}
var builder = new StringBuilder("git.exe", 259); var builder = new StringBuilder("git.exe", 259);
if (!PathFindOnPath(builder, null)) if (!PathFindOnPath(builder, null))
{
return null; return null;
}
var exePath = builder.ToString(); var exePath = builder.ToString();
if (!string.IsNullOrEmpty(exePath)) if (!string.IsNullOrEmpty(exePath))
{
return exePath; return exePath;
}
return null; return null;
} }
@ -175,7 +126,7 @@ namespace SourceGit.Native
break; break;
case "cmd": case "cmd":
return @"C:\Windows\System32\cmd.exe"; return "C:\\Windows\\System32\\cmd.exe";
case "wt": case "wt":
var wtFinder = new StringBuilder("wt.exe", 512); var wtFinder = new StringBuilder("wt.exe", 512);
if (PathFindOnPath(wtFinder, null)) if (PathFindOnPath(wtFinder, null))
@ -193,8 +144,8 @@ namespace SourceGit.Native
finder.VSCode(FindVSCode); finder.VSCode(FindVSCode);
finder.VSCodeInsiders(FindVSCodeInsiders); finder.VSCodeInsiders(FindVSCodeInsiders);
finder.VSCodium(FindVSCodium); finder.VSCodium(FindVSCodium);
finder.Fleet(() => $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Programs\Fleet\Fleet.exe"); finder.Fleet(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\Programs\\Fleet\\Fleet.exe");
finder.FindJetBrainsFromToolbox(() => $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\JetBrains\Toolbox"); finder.FindJetBrainsFromToolbox(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\JetBrains\\Toolbox");
finder.SublimeText(FindSublimeText); finder.SublimeText(FindSublimeText);
finder.TryAdd("Visual Studio", "vs", FindVisualStudio, GenerateCommandlineArgsForVisualStudio); finder.TryAdd("Visual Studio", "vs", FindVisualStudio, GenerateCommandlineArgsForVisualStudio);
return finder.Founded; return finder.Founded;
@ -277,12 +228,6 @@ namespace SourceGit.Native
}, DispatcherPriority.Render); }, 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 #region EXTERNAL_EDITOR_FINDER
private string FindVSCode() private string FindVSCode()
{ {
@ -293,7 +238,9 @@ namespace SourceGit.Native
// VSCode (system) // VSCode (system)
var systemVScode = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"); var systemVScode = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
if (systemVScode != null) if (systemVScode != null)
{
return systemVScode.GetValue("DisplayIcon") as string; return systemVScode.GetValue("DisplayIcon") as string;
}
var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.CurrentUser, Microsoft.Win32.RegistryHive.CurrentUser,
@ -302,7 +249,9 @@ namespace SourceGit.Native
// VSCode (user) // VSCode (user)
var vscode = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1"); var vscode = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1");
if (vscode != null) if (vscode != null)
{
return vscode.GetValue("DisplayIcon") as string; return vscode.GetValue("DisplayIcon") as string;
}
return string.Empty; return string.Empty;
} }
@ -316,7 +265,9 @@ namespace SourceGit.Native
// VSCode - Insiders (system) // VSCode - Insiders (system)
var systemVScodeInsiders = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"); var systemVScodeInsiders = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
if (systemVScodeInsiders != null) if (systemVScodeInsiders != null)
{
return systemVScodeInsiders.GetValue("DisplayIcon") as string; return systemVScodeInsiders.GetValue("DisplayIcon") as string;
}
var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.CurrentUser, Microsoft.Win32.RegistryHive.CurrentUser,
@ -325,7 +276,9 @@ namespace SourceGit.Native
// VSCode - Insiders (user) // VSCode - Insiders (user)
var vscodeInsiders = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1"); var vscodeInsiders = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1");
if (vscodeInsiders != null) if (vscodeInsiders != null)
{
return vscodeInsiders.GetValue("DisplayIcon") as string; return vscodeInsiders.GetValue("DisplayIcon") as string;
}
return string.Empty; return string.Empty;
} }
@ -337,9 +290,11 @@ namespace SourceGit.Native
Microsoft.Win32.RegistryView.Registry64); Microsoft.Win32.RegistryView.Registry64);
// VSCodium (system) // VSCodium (system)
var systemVSCodium = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1"); var systemVScodium = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1");
if (systemVSCodium != null) if (systemVScodium != null)
return systemVSCodium.GetValue("DisplayIcon") as string; {
return systemVScodium.GetValue("DisplayIcon") as string;
}
var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.CurrentUser, Microsoft.Win32.RegistryHive.CurrentUser,
@ -348,7 +303,9 @@ namespace SourceGit.Native
// VSCodium (user) // VSCodium (user)
var vscodium = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1"); var vscodium = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1");
if (vscodium != null) if (vscodium != null)
{
return vscodium.GetValue("DisplayIcon") as string; return vscodium.GetValue("DisplayIcon") as string;
}
return string.Empty; return string.Empty;
} }
@ -385,14 +342,16 @@ namespace SourceGit.Native
Microsoft.Win32.RegistryView.Registry64); Microsoft.Win32.RegistryView.Registry64);
// Get default class for VisualStudio.Launcher.sln - the handler for *.sln files // Get default class for VisualStudio.Launcher.sln - the handler for *.sln files
if (localMachine.OpenSubKey(@"SOFTWARE\Classes\VisualStudio.Launcher.sln\CLSID") is { } launcher) if (localMachine.OpenSubKey(@"SOFTWARE\Classes\VisualStudio.Launcher.sln\CLSID") is Microsoft.Win32.RegistryKey launcher)
{ {
// Get actual path to the executable // Get actual path to the executable
if (launcher.GetValue(string.Empty) is string CLSID && if (launcher.GetValue(string.Empty) is string CLSID &&
localMachine.OpenSubKey(@$"SOFTWARE\Classes\CLSID\{CLSID}\LocalServer32") is { } devenv && localMachine.OpenSubKey(@$"SOFTWARE\Classes\CLSID\{CLSID}\LocalServer32") is Microsoft.Win32.RegistryKey devenv &&
devenv.GetValue(string.Empty) is string localServer32) devenv.GetValue(string.Empty) is string localServer32)
{
return localServer32!.Trim('\"'); return localServer32!.Trim('\"');
} }
}
return string.Empty; return string.Empty;
} }

Some files were not shown because too many files have changed in this diff Show more