diff --git a/.editorconfig b/.editorconfig
index 22c741b9..f3c9a7bf 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -71,20 +71,20 @@ dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
-dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# private static fields should have s_ prefix
dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion
-dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style
-dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
@@ -93,14 +93,14 @@ dotnet_naming_style.private_static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
-dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
-dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# use accessibility modifiers
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d4117364..12792cf6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,14 +19,25 @@ jobs:
os: macos-latest
runtime: osx-arm64
- name : Linux
- os: ubuntu-20.04
+ os: ubuntu-latest
runtime: linux-x64
+ container: ubuntu:20.04
- name : Linux (arm64)
- os: ubuntu-20.04
+ os: ubuntu-latest
runtime: linux-arm64
+ container: ubuntu:20.04
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
+ container: ${{ matrix.container || '' }}
steps:
+ - name: Install common CLI tools
+ if: ${{ startsWith(matrix.runtime, 'linux-') }}
+ run: |
+ export DEBIAN_FRONTEND=noninteractive
+ ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
+ apt-get update
+ apt-get install -y sudo
+ sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup .NET
@@ -47,7 +58,7 @@ jobs:
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
sudo apt-get update
- sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
+ sudo apt-get install -y llvm gcc-aarch64-linux-gnu
- name: Build
run: dotnet build -c Release
- name: Publish
diff --git a/.github/workflows/localization-check.yml b/.github/workflows/localization-check.yml
index cc5201ab..8dcd61c8 100644
--- a/.github/workflows/localization-check.yml
+++ b/.github/workflows/localization-check.yml
@@ -4,7 +4,6 @@ on:
branches: [ develop ]
paths:
- 'src/Resources/Locales/**'
- - 'README.md'
workflow_dispatch:
workflow_call:
@@ -32,8 +31,8 @@ jobs:
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
if [ -n "$(git status --porcelain)" ]; then
- git add README.md TRANSLATION.md
- git commit -m 'doc: Update translation status and missing keys'
+ git add TRANSLATION.md src/Resources/Locales/*.axaml
+ git commit -m 'doc: Update translation status and sort locale files'
git push
else
echo "No changes to commit"
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 074c9544..2dfc97fd 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -9,10 +9,10 @@ on:
jobs:
windows:
name: Package Windows
- runs-on: ubuntu-latest
+ runs-on: windows-2019
strategy:
matrix:
- runtime: [win-x64, win-arm64]
+ runtime: [ win-x64, win-arm64 ]
steps:
- name: Checkout sources
uses: actions/checkout@v4
@@ -22,6 +22,7 @@ jobs:
name: sourcegit.${{ matrix.runtime }}
path: build/SourceGit
- name: Package
+ shell: bash
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
@@ -69,6 +70,7 @@ jobs:
linux:
name: Package Linux
runs-on: ubuntu-latest
+ container: ubuntu:20.04
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
@@ -77,9 +79,10 @@ jobs:
uses: actions/checkout@v4
- name: Download package dependencies
run: |
- sudo add-apt-repository universe
- sudo apt-get update
- sudo apt-get install desktop-file-utils rpm libfuse2
+ export DEBIAN_FRONTEND=noninteractive
+ ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
+ apt-get update
+ apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils
- name: Download build
uses: actions/download-artifact@v4
with:
@@ -89,6 +92,7 @@ jobs:
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
+ APPIMAGE_EXTRACT_AND_RUN: 1
run: |
mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
diff --git a/.gitignore b/.gitignore
index 0c66b11e..e686a534 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@ build/*.deb
build/*.rpm
build/*.AppImage
SourceGit.app/
+build.command
+src/Properties/launchSettings.json
diff --git a/LICENSE b/LICENSE
index dceab2d8..442ce085 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2024 sourcegit
+Copyright (c) 2025 sourcegit
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index e922c476..f9ba3072 100644
--- a/README.md
+++ b/README.md
@@ -11,16 +11,16 @@
* Supports Windows/macOS/Linux
* Opensource/Free
* Fast
-* Deutsch/English/Español/Français/Italiano/Português/Русский/简体中文/繁體中文
+* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)
* Built-in light/dark themes
* Customize theme
* Visual commit graph
* Supports SSH access with each remote
* GIT commands with GUI
* Clone/Fetch/Pull/Push...
- * Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
- * Amend/Reword
- * Interactive rebase (Basic)
+ * Merge/Rebase/Reset/Revert/Cherry-pick...
+ * Amend/Reword/Squash
+ * Interactive rebase
* Branches
* Remotes
* Tags
@@ -35,11 +35,14 @@
* Revision Diffs
* Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend
+* Git command logs
* Search commits
* GitFlow
* Git LFS
+* Bisect
* Issue Link
* Workspace
+* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING]
@@ -47,16 +50,13 @@
## Translation Status
-[](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md)
-
-> [!NOTE]
-> You can find the missing keys in [TRANSLATION.md](TRANSLATION.md)
+You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md)
## How to Use
-**To use this tool, you need to install Git(>=2.23.0) first.**
+**To use this tool, you need to install Git(>=2.25.1) first.**
-You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [Github Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
+You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
@@ -79,7 +79,7 @@ For **Windows** users:
```
> [!NOTE]
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
-* You can install the latest stable by `scoope` with follow commands:
+* You can install the latest stable by `scoop` with follow commands:
```shell
scoop bucket add extras
scoop install sourcegit
@@ -93,7 +93,7 @@ For **macOS** users:
brew tap ybeapps/homebrew-sourcegit
brew install --cask --no-quarantine sourcegit
```
-* If you want to install `SourceGit.app` from Github Release manually, you need run following command to make sure it works:
+* If you want to install `SourceGit.app` from GitHub Release manually, you need run following command to make sure it works:
```shell
sudo xattr -cr /Applications/SourceGit.app
```
@@ -132,15 +132,15 @@ For **Linux** users:
## OpenAI
-This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window.
+This software supports using OpenAI or other AI service that has an OpenAI compatible HTTP API to generate commit message. You need configurate the service in `Preference` window.
For `OpenAI`:
-* `Server` must be `https://api.openai.com/v1/chat/completions`
+* `Server` must be `https://api.openai.com/v1`
For other AI service:
-* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate`
+* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate`
* The `API Key` is optional that depends on the service
## External Tools
@@ -201,3 +201,7 @@ dotnet run --project src/SourceGit.csproj
Thanks to all the people who contribute.
[](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
+
+## Third-Party Components
+
+For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md).
diff --git a/SourceGit.sln b/SourceGit.sln
index cf761abd..624322f8 100644
--- a/SourceGit.sln
+++ b/SourceGit.sln
@@ -60,6 +60,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}"
ProjectSection(SolutionItems) = preProject
build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control
+ build\resources\deb\DEBIAN\preinst = build\resources\deb\DEBIAN\preinst
+ build\resources\deb\DEBIAN\prerm = build\resources\deb\DEBIAN\prerm
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}"
diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md
new file mode 100644
index 00000000..efc5676f
--- /dev/null
+++ b/THIRD-PARTY-LICENSES.md
@@ -0,0 +1,100 @@
+# Third-Party Licenses
+
+The project uses the following third-party libraries or assets
+
+## Packages
+
+### AvaloniaUI
+
+- **Source**: https://github.com/AvaloniaUI/Avalonia
+- **Version**: 11.2.8
+- **License**: MIT License
+- **License Link**: https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md
+
+### AvaloniaEdit
+
+- **Source**: https://github.com/AvaloniaUI/AvaloniaEdit
+- **Version**: 11.2.0
+- **License**: MIT License
+- **License Link**: https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/LICENSE
+
+### LiveChartsCore.SkiaSharpView.Avalonia
+
+- **Source**: https://github.com/beto-rodriguez/LiveCharts2
+- **Version**: 2.0.0-rc5.4
+- **License**: MIT License
+- **License Link**: https://github.com/beto-rodriguez/LiveCharts2/blob/master/LICENSE
+
+### TextMateSharp
+
+- **Source**: https://github.com/danipen/TextMateSharp
+- **Version**: 1.0.66
+- **License**: MIT License
+- **License Link**: https://github.com/danipen/TextMateSharp/blob/master/LICENSE.md
+
+### OpenAI .NET SDK
+
+- **Source**: https://github.com/openai/openai-dotnet
+- **Version**: 2.2.0-beta.4
+- **License**: MIT License
+- **License Link**: https://github.com/openai/openai-dotnet/blob/main/LICENSE
+
+### Azure.AI.OpenAI
+
+- **Source**: https://github.com/Azure/azure-sdk-for-net
+- **Version**: 2.2.0-beta.4
+- **License**: MIT License
+- **License Link**: https://github.com/Azure/azure-sdk-for-net/blob/main/LICENSE.txt
+
+### BitMiracle.LibTiff.NET
+
+- **Source**: https://github.com/BitMiracle/libtiff.net
+- **Version**: 2.4.660
+- **License**: New BSD License
+- **License Link**: https://github.com/BitMiracle/libtiff.net/blob/master/license.txt
+
+### Pfim
+
+- **Source**: https://github.com/nickbabcock/Pfim
+- **Version**: 0.11.3
+- **License**: MIT License
+- **License Link**: https://github.com/nickbabcock/Pfim/blob/master/LICENSE.txt
+
+## Fonts
+
+### JetBrainsMono
+
+- **Source**: https://github.com/JetBrains/JetBrainsMono
+- **Commit**: v2.304
+- **License**: SIL Open Font License, Version 1.1
+- **License Link**: https://github.com/JetBrains/JetBrainsMono/blob/v2.304/OFL.txt
+
+## Grammar Files
+
+### haxe-TmLanguage
+
+- **Source**: https://github.com/vshaxe/haxe-TmLanguage
+- **Commit**: ddad8b4c6d0781ac20be0481174ec1be772c5da5
+- **License**: MIT License
+- **License Link**: https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md
+
+### coc-toml
+
+- **Source**: https://github.com/kkiyama117/coc-toml
+- **Commit**: aac3e0c65955c03314b2733041b19f903b7cc447
+- **License**: MIT License
+- **License Link**: https://github.com/kkiyama117/coc-toml/blob/aac3e0c65955c03314b2733041b19f903b7cc447/LICENSE
+
+### eclipse-buildship
+
+- **Source**: https://github.com/eclipse/buildship
+- **Commit**: 6bb773e7692f913dec27105129ebe388de34e68b
+- **License**: Eclipse Public License 1.0
+- **License Link**: https://github.com/eclipse-buildship/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/README.md
+
+### vscode-jsp-lang
+
+- **Source**: https://github.com/samuel-weinhardt/vscode-jsp-lang
+- **Commit**: 0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355
+- **License**: MIT License
+- **License Link**: https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/LICENSE
diff --git a/TRANSLATION.md b/TRANSLATION.md
index aa42d82b..97a4258c 100644
--- a/TRANSLATION.md
+++ b/TRANSLATION.md
@@ -1,205 +1,644 @@
-### de_DE.axaml: 100.00%
+# Translation Status
+This document shows the translation status of each locale file in the repository.
+
+## Details
+
+### 
+
+### 
+
+### 
+
+### 
-Missing Keys
-
+Missing keys in fr_FR.axaml
+- Text.AddToIgnore
+- Text.AddToIgnore.Pattern
+- Text.AddToIgnore.Storage
+- 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.WarnLostCommits
+- Text.Checkout.WithFastForward
+- Text.Checkout.WithFastForward.Upstream
+- Text.CommitCM.CopyAuthor
+- Text.CommitCM.CopyCommitter
+- Text.CommitCM.CopySubject
+- Text.CommitCM.PushRevision
+- Text.CommitDetail.Changes.Count
+- Text.CommitMessageTextBox.SubjectCount
+- Text.Configure.CustomAction.Arguments.Tip
+- Text.Configure.CustomAction.InputControls
+- Text.Configure.CustomAction.InputControls.Edit
+- Text.Configure.CustomAction.InputControls.Tip
+- Text.Configure.CustomAction.Scope.Tag
+- Text.Configure.Git.PreferredMergeMode
+- Text.ConfigureCustomActionControls
+- Text.ConfigureCustomActionControls.CheckedValue
+- Text.ConfigureCustomActionControls.CheckedValue.Tip
+- Text.ConfigureCustomActionControls.Description
+- Text.ConfigureCustomActionControls.Description.Tip
+- Text.ConfigureCustomActionControls.DefaultValue
+- Text.ConfigureCustomActionControls.IsFolder
+- Text.ConfigureCustomActionControls.Label
+- Text.ConfigureCustomActionControls.Type
+- Text.ConfirmEmptyCommit.Continue
+- Text.ConfirmEmptyCommit.NoLocalChanges
+- Text.ConfirmEmptyCommit.StageAllThenCommit
+- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.CreateBranch.OverwriteExisting
+- Text.DeinitSubmodule
+- Text.DeinitSubmodule.Force
+- Text.DeinitSubmodule.Path
+- Text.Diff.Submodule.Deleted
+- Text.ExecuteCustomAction.Target
+- Text.ExecuteCustomAction.Repository
+- 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.Search.ByPath
+- Text.Repository.ShowSubmodulesAsTree
+- Text.Repository.ViewLogs
+- Text.Repository.Visit
+- Text.ResetWithoutCheckout
+- Text.ResetWithoutCheckout.MoveTo
+- Text.ResetWithoutCheckout.Target
+- Text.Stash.Mode
+- Text.StashCM.CopyMessage
+- Text.Submodule.Deinit
+- Text.Submodule.Status
+- Text.Submodule.Status.Modified
+- Text.Submodule.Status.NotInited
+- Text.Submodule.Status.RevisionChanged
+- Text.Submodule.Status.Unmerged
+- Text.Submodule.URL
+- Text.TagCM.CustomAction
+- Text.ViewLogs
+- Text.ViewLogs.Clear
+- Text.ViewLogs.CopyLog
+- Text.ViewLogs.Delete
+- Text.WorkingCopy.AddToGitIgnore.InFolder
+- Text.WorkingCopy.ConfirmCommitWithDetachedHead
+- Text.WorkingCopy.ConfirmCommitWithFilter
+- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
+- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
+- Text.WorkingCopy.Conflicts.UseMine
+- Text.WorkingCopy.Conflicts.UseTheirs
+- Text.WorkingCopy.ResetAuthor
-### es_ES.axaml: 100.00%
-
+### 
-Missing Keys
-
+Missing keys in it_IT.axaml
+- Text.AddToIgnore
+- Text.AddToIgnore.Pattern
+- Text.AddToIgnore.Storage
+- Text.Avatar.Load
+- Text.BranchCM.ResetToSelectedCommit
+- Text.Checkout.WarnLostCommits
+- Text.Checkout.WithFastForward
+- Text.Checkout.WithFastForward.Upstream
+- Text.CommitCM.PushRevision
+- Text.CommitDetail.Changes.Count
+- Text.Configure.CustomAction.Arguments.Tip
+- Text.Configure.CustomAction.InputControls
+- Text.Configure.CustomAction.InputControls.Edit
+- Text.Configure.CustomAction.InputControls.Tip
+- Text.Configure.CustomAction.Scope.Tag
+- Text.ConfigureCustomActionControls
+- Text.ConfigureCustomActionControls.CheckedValue
+- Text.ConfigureCustomActionControls.CheckedValue.Tip
+- Text.ConfigureCustomActionControls.Description
+- Text.ConfigureCustomActionControls.Description.Tip
+- Text.ConfigureCustomActionControls.DefaultValue
+- Text.ConfigureCustomActionControls.IsFolder
+- Text.ConfigureCustomActionControls.Label
+- Text.ConfigureCustomActionControls.Type
+- Text.CreateBranch.OverwriteExisting
+- Text.DeinitSubmodule
+- Text.DeinitSubmodule.Force
+- Text.DeinitSubmodule.Path
+- Text.Diff.Submodule.Deleted
+- Text.ExecuteCustomAction.Target
+- Text.ExecuteCustomAction.Repository
+- 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.Repository.Search.ByPath
+- Text.ResetWithoutCheckout
+- Text.ResetWithoutCheckout.MoveTo
+- Text.ResetWithoutCheckout.Target
+- Text.Stash.Mode
+- Text.StashCM.CopyMessage
+- Text.Submodule.Deinit
+- Text.TagCM.CustomAction
+- Text.WorkingCopy.AddToGitIgnore.InFolder
+- Text.WorkingCopy.ConfirmCommitWithDetachedHead
+- Text.WorkingCopy.ResetAuthor
-### fr_FR.axaml: 94.69%
-
+### 
-Missing Keys
+Missing keys in ja_JP.axaml
-- Text.InProgress.CherryPick.Head
-- Text.InProgress.Merge.Operating
-- Text.InProgress.Rebase.StoppedAt
-- Text.InProgress.Revert.Head
-- Text.Merge.Source
-- Text.MergeMultiple
-- Text.MergeMultiple.CommitChanges
-- Text.MergeMultiple.Strategy
-- Text.MergeMultiple.Targets
-- Text.Preferences.Appearance.FontSize
-- Text.Preferences.Appearance.FontSize.Default
-- Text.Preferences.Appearance.FontSize.Editor
-- Text.Preferences.General.DateFormat
-- Text.Preferences.General.ShowChildren
-- Text.Preferences.Git.SSLVerify
-- Text.Repository.CustomActions
+- Text.AddToIgnore
+- Text.AddToIgnore.Pattern
+- Text.AddToIgnore.Storage
+- 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.WarnLostCommits
+- Text.Checkout.WithFastForward
+- Text.Checkout.WithFastForward.Upstream
+- Text.CommitCM.CopyAuthor
+- Text.CommitCM.CopyCommitter
+- Text.CommitCM.CopySubject
+- Text.CommitCM.PushRevision
+- Text.CommitDetail.Changes.Count
+- Text.CommitMessageTextBox.SubjectCount
+- Text.Configure.CustomAction.Arguments.Tip
+- Text.Configure.CustomAction.InputControls
+- Text.Configure.CustomAction.InputControls.Edit
+- Text.Configure.CustomAction.InputControls.Tip
+- Text.Configure.CustomAction.Scope.Tag
+- Text.Configure.Git.PreferredMergeMode
+- Text.ConfigureCustomActionControls
+- Text.ConfigureCustomActionControls.CheckedValue
+- Text.ConfigureCustomActionControls.CheckedValue.Tip
+- Text.ConfigureCustomActionControls.Description
+- Text.ConfigureCustomActionControls.Description.Tip
+- Text.ConfigureCustomActionControls.DefaultValue
+- Text.ConfigureCustomActionControls.IsFolder
+- Text.ConfigureCustomActionControls.Label
+- Text.ConfigureCustomActionControls.Type
+- Text.ConfirmEmptyCommit.Continue
+- Text.ConfirmEmptyCommit.NoLocalChanges
+- Text.ConfirmEmptyCommit.StageAllThenCommit
+- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.CreateBranch.OverwriteExisting
+- Text.DeinitSubmodule
+- Text.DeinitSubmodule.Force
+- Text.DeinitSubmodule.Path
+- Text.Diff.Submodule.Deleted
+- Text.ExecuteCustomAction.Target
+- Text.ExecuteCustomAction.Repository
+- 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.FilterCommits.Default
-- Text.Repository.FilterCommits.Exclude
-- Text.Repository.FilterCommits.Include
-- Text.Repository.HistoriesLayout
-- Text.Repository.HistoriesLayout.Horizontal
-- Text.Repository.HistoriesLayout.Vertical
-- Text.Repository.HistoriesOrder
-- Text.Repository.HistoriesOrder.ByDate
-- Text.Repository.HistoriesOrder.Topo
-- Text.Repository.Skip
-- Text.Repository.Tags.OrderByCreatorDate
-- Text.Repository.Tags.OrderByNameAsc
-- Text.Repository.Tags.OrderByNameDes
-- Text.Repository.Tags.Sort
-- Text.Repository.UseRelativeTimeInHistories
-- Text.ScanRepositories
-- Text.SetUpstream
-- Text.SetUpstream.Local
-- Text.SetUpstream.Unset
-- Text.SetUpstream.Upstream
-- Text.SHALinkCM.NavigateTo
-- Text.WorkingCopy.CommitToEdit
+- Text.Repository.Search.ByContent
+- Text.Repository.Search.ByPath
+- Text.Repository.ShowSubmodulesAsTree
+- Text.Repository.ViewLogs
+- Text.Repository.Visit
+- Text.ResetWithoutCheckout
+- Text.ResetWithoutCheckout.MoveTo
+- Text.ResetWithoutCheckout.Target
+- Text.Stash.Mode
+- Text.StashCM.CopyMessage
+- Text.Submodule.Deinit
+- Text.Submodule.Status
+- Text.Submodule.Status.Modified
+- Text.Submodule.Status.NotInited
+- Text.Submodule.Status.RevisionChanged
+- Text.Submodule.Status.Unmerged
+- Text.Submodule.URL
+- Text.TagCM.CustomAction
+- Text.ViewLogs
+- Text.ViewLogs.Clear
+- Text.ViewLogs.CopyLog
+- Text.ViewLogs.Delete
+- Text.WorkingCopy.AddToGitIgnore.InFolder
+- Text.WorkingCopy.ConfirmCommitWithDetachedHead
+- Text.WorkingCopy.ConfirmCommitWithFilter
+- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
+- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
+- Text.WorkingCopy.Conflicts.UseMine
+- Text.WorkingCopy.Conflicts.UseTheirs
+- Text.WorkingCopy.ResetAuthor
-### it_IT.axaml: 93.32%
-
+### 
-Missing Keys
-
-- Text.BranchCM.MergeMultiBranches
-- Text.CommitCM.Merge
-- Text.CommitCM.MergeMultiple
-- Text.CommitDetail.Files.Search
-- Text.CommitDetail.Info.Children
-- Text.Configure.IssueTracker.AddSampleGiteeIssue
-- Text.Configure.IssueTracker.AddSampleGiteePullRequest
-- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
-- Text.Configure.OpenAI.Preferred
-- Text.Configure.OpenAI.Preferred.Tip
-- Text.Diff.UseBlockNavigation
-- Text.Fetch.Force
-- Text.FileCM.ResolveUsing
-- Text.InProgress.CherryPick.Head
-- Text.InProgress.Merge.Operating
-- Text.InProgress.Rebase.StoppedAt
-- Text.InProgress.Revert.Head
-- Text.Merge.Source
-- Text.MergeMultiple
-- Text.MergeMultiple.CommitChanges
-- Text.MergeMultiple.Strategy
-- Text.MergeMultiple.Targets
-- Text.Preferences.General.DateFormat
-- Text.Preferences.General.ShowChildren
-- Text.Preferences.Git.SSLVerify
-- Text.Repository.FilterCommits
-- Text.Repository.FilterCommits.Default
-- Text.Repository.FilterCommits.Exclude
-- Text.Repository.FilterCommits.Include
-- Text.Repository.HistoriesLayout
-- Text.Repository.HistoriesLayout.Horizontal
-- Text.Repository.HistoriesLayout.Vertical
-- Text.Repository.HistoriesOrder
-- Text.Repository.HistoriesOrder.ByDate
-- Text.Repository.HistoriesOrder.Topo
-- Text.Repository.OnlyHighlightCurrentBranchInHistories
-- Text.Repository.Skip
-- Text.Repository.Tags.OrderByCreatorDate
-- Text.Repository.Tags.OrderByNameAsc
-- Text.Repository.Tags.OrderByNameDes
-- Text.Repository.Tags.Sort
-- Text.Repository.UseRelativeTimeInHistories
-- Text.SetUpstream
-- Text.SetUpstream.Local
-- Text.SetUpstream.Unset
-- Text.SetUpstream.Upstream
-- Text.SHALinkCM.CopySHA
-- Text.SHALinkCM.NavigateTo
-- Text.WorkingCopy.CommitToEdit
-
-
-
-### pt_BR.axaml: 94.41%
-
-
-
-Missing Keys
+Missing keys in pt_BR.axaml
+- Text.AddToIgnore
+- Text.AddToIgnore.Pattern
+- Text.AddToIgnore.Storage
+- Text.AIAssistant.Regen
+- Text.AIAssistant.Use
+- Text.ApplyStash
+- Text.ApplyStash.DropAfterApply
+- Text.ApplyStash.RestoreIndex
+- Text.ApplyStash.Stash
+- Text.Avatar.Load
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
+- Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
+- Text.BranchCM.ResetToSelectedCommit
+- Text.BranchUpstreamInvalid
+- Text.Checkout.RecurseSubmodules
+- Text.Checkout.WarnLostCommits
+- Text.Checkout.WithFastForward
+- Text.Checkout.WithFastForward.Upstream
+- Text.Clone.RecurseSubmodules
+- Text.CommitCM.CopyAuthor
+- Text.CommitCM.CopyCommitter
+- Text.CommitCM.CopySubject
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
+- Text.CommitCM.PushRevision
+- Text.CommitDetail.Changes.Count
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
+- Text.CommitMessageTextBox.SubjectCount
+- Text.Configure.CustomAction.Arguments.Tip
+- Text.Configure.CustomAction.InputControls
+- Text.Configure.CustomAction.InputControls.Edit
+- Text.Configure.CustomAction.InputControls.Tip
+- Text.Configure.CustomAction.Scope.Branch
+- Text.Configure.CustomAction.Scope.Tag
+- Text.Configure.CustomAction.WaitForExit
+- Text.Configure.Git.PreferredMergeMode
- Text.Configure.IssueTracker.AddSampleGiteeIssue
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
+- Text.ConfigureCustomActionControls
+- Text.ConfigureCustomActionControls.CheckedValue
+- Text.ConfigureCustomActionControls.CheckedValue.Tip
+- Text.ConfigureCustomActionControls.Description
+- Text.ConfigureCustomActionControls.Description.Tip
+- Text.ConfigureCustomActionControls.DefaultValue
+- Text.ConfigureCustomActionControls.IsFolder
+- Text.ConfigureCustomActionControls.Label
+- Text.ConfigureCustomActionControls.Type
+- Text.ConfirmEmptyCommit.Continue
+- Text.ConfirmEmptyCommit.NoLocalChanges
+- Text.ConfirmEmptyCommit.StageAllThenCommit
+- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.CopyFullPath
+- Text.CreateBranch.Name.WarnSpace
+- Text.CreateBranch.OverwriteExisting
+- Text.DeinitSubmodule
+- Text.DeinitSubmodule.Force
+- Text.DeinitSubmodule.Path
+- Text.DeleteRepositoryNode.Path
+- Text.DeleteRepositoryNode.TipForGroup
+- Text.DeleteRepositoryNode.TipForRepository
+- Text.Diff.First
+- Text.Diff.Last
+- Text.Diff.Submodule.Deleted
- Text.Diff.UseBlockNavigation
+- Text.ExecuteCustomAction.Target
+- Text.ExecuteCustomAction.Repository
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
+- Text.GitFlow.FinishWithPush
+- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.Clone
+- Text.Hotkeys.Global.SwitchWorkspace
+- Text.Hotkeys.Global.SwitchTab
+- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head
+- Text.Launcher.Workspaces
+- Text.Launcher.Pages
+- Text.Merge.Edit
- Text.Merge.Source
- Text.MergeMultiple
- Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets
+- Text.Preferences.AI.Streaming
+- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
+- Text.Preferences.General.ShowTagsInGraph
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Preferences.Git.SSLVerify
+- Text.Pull.RecurseSubmodules
+- Text.Push.Revision
+- Text.Push.Revision.Title
+- Text.Repository.BranchSort
+- Text.Repository.BranchSort.ByCommitterDate
+- Text.Repository.BranchSort.ByName
+- Text.Repository.ClearStashes
- Text.Repository.FilterCommits
- Text.Repository.HistoriesLayout
- Text.Repository.HistoriesLayout.Horizontal
- Text.Repository.HistoriesLayout.Vertical
- Text.Repository.HistoriesOrder
+- Text.Repository.Notifications.Clear
- Text.Repository.OnlyHighlightCurrentBranchInHistories
+- Text.Repository.Search.ByContent
+- Text.Repository.Search.ByPath
+- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.Skip
- Text.Repository.Tags.OrderByCreatorDate
-- Text.Repository.Tags.OrderByNameAsc
-- Text.Repository.Tags.OrderByNameDes
+- Text.Repository.Tags.OrderByName
- Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories
+- Text.Repository.ViewLogs
+- Text.Repository.Visit
+- Text.ResetWithoutCheckout
+- Text.ResetWithoutCheckout.MoveTo
+- Text.ResetWithoutCheckout.Target
- Text.SetUpstream
- Text.SetUpstream.Local
- Text.SetUpstream.Unset
- Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo
+- Text.Stash.Mode
+- Text.StashCM.CopyMessage
+- Text.StashCM.SaveAsPatch
+- Text.Submodule.Deinit
+- Text.Submodule.Status
+- Text.Submodule.Status.Modified
+- Text.Submodule.Status.NotInited
+- Text.Submodule.Status.RevisionChanged
+- Text.Submodule.Status.Unmerged
+- Text.Submodule.URL
+- Text.TagCM.CustomAction
+- Text.ViewLogs
+- Text.ViewLogs.Clear
+- Text.ViewLogs.CopyLog
+- Text.ViewLogs.Delete
+- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.CommitToEdit
+- Text.WorkingCopy.ConfirmCommitWithDetachedHead
+- Text.WorkingCopy.ConfirmCommitWithFilter
+- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
+- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
+- Text.WorkingCopy.Conflicts.UseMine
+- Text.WorkingCopy.Conflicts.UseTheirs
+- Text.WorkingCopy.ResetAuthor
+- Text.WorkingCopy.SignOff
-### ru_RU.axaml: 100.00%
+### 
+### 
-Missing Keys
-
+Missing keys in ta_IN.axaml
+- Text.AddToIgnore
+- Text.AddToIgnore.Pattern
+- Text.AddToIgnore.Storage
+- 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.WarnLostCommits
+- Text.Checkout.WithFastForward
+- Text.Checkout.WithFastForward.Upstream
+- Text.CommitCM.CopyAuthor
+- Text.CommitCM.CopyCommitter
+- Text.CommitCM.CopySubject
+- Text.CommitCM.PushRevision
+- Text.CommitDetail.Changes.Count
+- Text.CommitMessageTextBox.SubjectCount
+- Text.Configure.CustomAction.Arguments.Tip
+- Text.Configure.CustomAction.InputControls
+- Text.Configure.CustomAction.InputControls.Edit
+- Text.Configure.CustomAction.InputControls.Tip
+- Text.Configure.CustomAction.Scope.Tag
+- Text.Configure.Git.PreferredMergeMode
+- Text.ConfigureCustomActionControls
+- Text.ConfigureCustomActionControls.CheckedValue
+- Text.ConfigureCustomActionControls.CheckedValue.Tip
+- Text.ConfigureCustomActionControls.Description
+- Text.ConfigureCustomActionControls.Description.Tip
+- Text.ConfigureCustomActionControls.DefaultValue
+- Text.ConfigureCustomActionControls.IsFolder
+- Text.ConfigureCustomActionControls.Label
+- Text.ConfigureCustomActionControls.Type
+- Text.ConfirmEmptyCommit.Continue
+- Text.ConfirmEmptyCommit.NoLocalChanges
+- Text.ConfirmEmptyCommit.StageAllThenCommit
+- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.CreateBranch.OverwriteExisting
+- Text.DeinitSubmodule
+- Text.DeinitSubmodule.Force
+- Text.DeinitSubmodule.Path
+- Text.Diff.Submodule.Deleted
+- Text.ExecuteCustomAction.Target
+- Text.ExecuteCustomAction.Repository
+- 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.Search.ByPath
+- Text.Repository.ShowSubmodulesAsTree
+- Text.Repository.ViewLogs
+- Text.Repository.Visit
+- Text.ResetWithoutCheckout
+- Text.ResetWithoutCheckout.MoveTo
+- Text.ResetWithoutCheckout.Target
+- Text.Stash.Mode
+- Text.StashCM.CopyMessage
+- Text.Submodule.Deinit
+- Text.Submodule.Status
+- Text.Submodule.Status.Modified
+- Text.Submodule.Status.NotInited
+- Text.Submodule.Status.RevisionChanged
+- Text.Submodule.Status.Unmerged
+- Text.Submodule.URL
+- Text.TagCM.CustomAction
+- Text.UpdateSubmodules.Target
+- Text.ViewLogs
+- Text.ViewLogs.Clear
+- Text.ViewLogs.CopyLog
+- Text.ViewLogs.Delete
+- Text.WorkingCopy.AddToGitIgnore.InFolder
+- Text.WorkingCopy.ConfirmCommitWithDetachedHead
+- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
+- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
+- Text.WorkingCopy.Conflicts.UseMine
+- Text.WorkingCopy.Conflicts.UseTheirs
+- Text.WorkingCopy.ResetAuthor
-### zh_CN.axaml: 100.00%
-
+### 
-Missing Keys
-
+Missing keys in uk_UA.axaml
+- Text.AddToIgnore
+- Text.AddToIgnore.Pattern
+- Text.AddToIgnore.Storage
+- 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.WarnLostCommits
+- Text.Checkout.WithFastForward
+- Text.Checkout.WithFastForward.Upstream
+- Text.CommitCM.CopyAuthor
+- Text.CommitCM.CopyCommitter
+- Text.CommitCM.CopySubject
+- Text.CommitCM.PushRevision
+- Text.CommitDetail.Changes.Count
+- Text.CommitMessageTextBox.SubjectCount
+- Text.Configure.CustomAction.Arguments.Tip
+- Text.Configure.CustomAction.InputControls
+- Text.Configure.CustomAction.InputControls.Edit
+- Text.Configure.CustomAction.InputControls.Tip
+- Text.Configure.CustomAction.Scope.Tag
+- Text.ConfigureCustomActionControls
+- Text.ConfigureCustomActionControls.CheckedValue
+- Text.ConfigureCustomActionControls.CheckedValue.Tip
+- Text.ConfigureCustomActionControls.Description
+- Text.ConfigureCustomActionControls.Description.Tip
+- Text.ConfigureCustomActionControls.DefaultValue
+- Text.ConfigureCustomActionControls.IsFolder
+- Text.ConfigureCustomActionControls.Label
+- Text.ConfigureCustomActionControls.Type
+- Text.ConfigureWorkspace.Name
+- Text.CreateBranch.OverwriteExisting
+- Text.DeinitSubmodule
+- Text.DeinitSubmodule.Force
+- Text.DeinitSubmodule.Path
+- Text.Diff.Submodule.Deleted
+- Text.ExecuteCustomAction.Target
+- Text.ExecuteCustomAction.Repository
+- 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.Search.ByPath
+- Text.Repository.ShowSubmodulesAsTree
+- Text.Repository.ViewLogs
+- Text.Repository.Visit
+- Text.ResetWithoutCheckout
+- Text.ResetWithoutCheckout.MoveTo
+- Text.ResetWithoutCheckout.Target
+- Text.Stash.Mode
+- Text.StashCM.CopyMessage
+- Text.Submodule.Deinit
+- Text.Submodule.Status
+- Text.Submodule.Status.Modified
+- Text.Submodule.Status.NotInited
+- Text.Submodule.Status.RevisionChanged
+- Text.Submodule.Status.Unmerged
+- Text.Submodule.URL
+- Text.TagCM.CustomAction
+- Text.ViewLogs
+- Text.ViewLogs.Clear
+- Text.ViewLogs.CopyLog
+- Text.ViewLogs.Delete
+- Text.WorkingCopy.AddToGitIgnore.InFolder
+- Text.WorkingCopy.ConfirmCommitWithDetachedHead
+- Text.WorkingCopy.ResetAuthor
-### zh_TW.axaml: 100.00%
+### 
-
-
-Missing Keys
-
-
-
-
+### 
\ No newline at end of file
diff --git a/VERSION b/VERSION
index d5cfddc3..56ea42f8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2025.03
\ No newline at end of file
+2025.24
\ No newline at end of file
diff --git a/build/README.md b/build/README.md
index b4358a55..17305edf 100644
--- a/build/README.md
+++ b/build/README.md
@@ -12,4 +12,4 @@
dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj
```
> [!NOTE]
-> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replece the `$DESTINATION_FOLDER` with the real path that will store the output executable files.
+> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replace the `$DESTINATION_FOLDER` with the real path that will store the output executable files.
diff --git a/build/resources/deb/DEBIAN/control b/build/resources/deb/DEBIAN/control
index f553db8b..71786b43 100755
--- a/build/resources/deb/DEBIAN/control
+++ b/build/resources/deb/DEBIAN/control
@@ -1,7 +1,8 @@
Package: sourcegit
-Version: 8.23
+Version: 2025.10
Priority: optional
Depends: libx11-6, libice6, libsm6, libicu | libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu69 | libicu68 | libicu67 | libicu66 | libicu65 | libicu63 | libicu60 | libicu57 | libicu55 | libicu52, xdg-utils
Architecture: amd64
+Installed-Size: 60440
Maintainer: longshuang@msn.cn
Description: Open-source & Free Git GUI Client
diff --git a/build/resources/deb/DEBIAN/preinst b/build/resources/deb/DEBIAN/preinst
new file mode 100755
index 00000000..a93f8090
--- /dev/null
+++ b/build/resources/deb/DEBIAN/preinst
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+set -e
+
+# summary of how this script can be called:
+# * `install'
+# * `install'
+# * `upgrade'
+# * `abort-upgrade'
+# for details, see http://www.debian.org/doc/debian-policy/
+
+case "$1" in
+ install|upgrade)
+ # Check if SourceGit is running and stop it
+ if pgrep -f '/opt/sourcegit/sourcegit' > /dev/null; then
+ echo "Stopping running SourceGit instance..."
+ pkill -f '/opt/sourcegit/sourcegit' || true
+ # Give the process a moment to terminate
+ sleep 1
+ fi
+ ;;
+
+ abort-upgrade)
+ ;;
+
+ *)
+ echo "preinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/build/resources/deb/DEBIAN/prerm b/build/resources/deb/DEBIAN/prerm
new file mode 100755
index 00000000..c2c9e4f0
--- /dev/null
+++ b/build/resources/deb/DEBIAN/prerm
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+set -e
+
+# summary of how this script can be called:
+# * `remove'
+# * `upgrade'
+# * `failed-upgrade'
+# * `remove' `in-favour'
+# * `deconfigure' `in-favour'
+# `removing'
+#
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+ remove|upgrade|deconfigure)
+ if pgrep -f '/opt/sourcegit/sourcegit' > /dev/null; then
+ echo "Stopping running SourceGit instance..."
+ pkill -f '/opt/sourcegit/sourcegit' || true
+ # Give the process a moment to terminate
+ sleep 1
+ fi
+ ;;
+
+ failed-upgrade)
+ ;;
+
+ *)
+ echo "prerm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/build/scripts/localization-check.js b/build/scripts/localization-check.js
index 45db82be..8d636b5b 100644
--- a/build/scripts/localization-check.js
+++ b/build/scripts/localization-check.js
@@ -6,7 +6,6 @@ const repoRoot = path.join(__dirname, '../../');
const localesDir = path.join(repoRoot, 'src/Resources/Locales');
const enUSFile = path.join(localesDir, 'en_US.axaml');
const outputFile = path.join(repoRoot, 'TRANSLATION.md');
-const readmeFile = path.join(repoRoot, 'README.md');
const parser = new xml2js.Parser();
@@ -15,45 +14,70 @@ async function parseXml(filePath) {
return parser.parseStringPromise(data);
}
+async function filterAndSortTranslations(localeData, enUSKeys, enUSData) {
+ const strings = localeData.ResourceDictionary['x:String'];
+ // Remove keys that don't exist in English file
+ const filtered = strings.filter(item => enUSKeys.has(item.$['x:Key']));
+
+ // Sort based on the key order in English file
+ const enUSKeysArray = enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']);
+ filtered.sort((a, b) => {
+ const aIndex = enUSKeysArray.indexOf(a.$['x:Key']);
+ const bIndex = enUSKeysArray.indexOf(b.$['x:Key']);
+ return aIndex - bIndex;
+ });
+
+ return filtered;
+}
+
async function calculateTranslationRate() {
const enUSData = await parseXml(enUSFile);
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
-
- const translationRates = [];
- const badges = [];
-
const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml'));
- // Add en_US badge first
- badges.push(`[](TRANSLATION.md)`);
+ const lines = [];
+
+ lines.push('# Translation Status');
+ lines.push('This document shows the translation status of each locale file in the repository.');
+ lines.push(`## Details`);
+ lines.push(`### `);
for (const file of files) {
+ const locale = file.replace('.axaml', '').replace('_', '__');
const filePath = path.join(localesDir, file);
const localeData = await parseXml(filePath);
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
-
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
- const translationRate = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
- translationRates.push(`### ${file}: ${translationRate.toFixed(2)}%\n`);
- translationRates.push(`\nMissing Keys
\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n `);
+ // Sort and clean up extra translations
+ const sortedAndCleaned = await filterAndSortTranslations(localeData, enUSKeys, enUSData);
+ localeData.ResourceDictionary['x:String'] = sortedAndCleaned;
- // Add badges
- const locale = file.replace('.axaml', '').replace('_', '__');
- const badgeColor = translationRate === 100 ? 'brightgreen' : translationRate >= 75 ? 'yellow' : 'red';
- badges.push(`[}%25-${badgeColor})](TRANSLATION.md)`);
+ // Save the updated file
+ const builder = new xml2js.Builder({
+ headless: true,
+ renderOpts: { pretty: true, indent: ' ' }
+ });
+ let xmlStr = builder.buildObject(localeData);
+
+ // Add an empty line before the first x:String
+ xmlStr = xmlStr.replace(' 0) {
+ const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
+ const badgeColor = progress >= 75 ? 'yellow' : 'red';
+
+ lines.push(`### }%25-${badgeColor})`);
+ lines.push(`\nMissing keys in ${file}
\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n `)
+ } else {
+ lines.push(`### `);
+ }
}
- console.log(translationRates.join('\n\n'));
-
- await fs.writeFile(outputFile, translationRates.join('\n\n') + '\n', 'utf8');
-
- // Update README.md
- let readmeContent = await fs.readFile(readmeFile, 'utf8');
- const badgeSection = `## Translation Status\n\n${badges.join(' ')}`;
- console.log(badgeSection);
- readmeContent = readmeContent.replace(/## Translation Status\n\n.*\n\n/, badgeSection + '\n\n');
- await fs.writeFile(readmeFile, readmeContent, 'utf8');
+ const content = lines.join('\n\n');
+ console.log(content);
+ await fs.writeFile(outputFile, content, 'utf8');
}
calculateTranslationRate().catch(err => console.error(err));
diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh
index 5abb058b..1b4adbdc 100755
--- a/build/scripts/package.linux.sh
+++ b/build/scripts/package.linux.sh
@@ -56,8 +56,15 @@ cp -f SourceGit/* resources/deb/opt/sourcegit
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
cp -r resources/_common/applications resources/deb/usr/share
cp -r resources/_common/icons resources/deb/usr/share
-sed -i -e "s/^Version:.*/Version: $VERSION/" -e "s/^Architecture:.*/Architecture: $arch/" resources/deb/DEBIAN/control
-dpkg-deb --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
+# Calculate installed size in KB
+installed_size=$(du -sk resources/deb | cut -f1)
+# Update the control file
+sed -i -e "s/^Version:.*/Version: $VERSION/" \
+ -e "s/^Architecture:.*/Architecture: $arch/" \
+ -e "s/^Installed-Size:.*/Installed-Size: $installed_size/" \
+ resources/deb/DEBIAN/control
+# Build deb package with gzip compression
+dpkg-deb -Zgzip --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./
diff --git a/build/scripts/package.windows.sh b/build/scripts/package.windows.sh
index 6bd3879b..c22a9d35 100755
--- a/build/scripts/package.windows.sh
+++ b/build/scripts/package.windows.sh
@@ -9,4 +9,8 @@ cd build
rm -rf SourceGit/*.pdb
-zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
+if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
+ powershell -Command "Compress-Archive -Path SourceGit -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force"
+else
+ zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
+fi
diff --git a/src/App.Commands.cs b/src/App.Commands.cs
index 85a75829..22e9fb51 100644
--- a/src/App.Commands.cs
+++ b/src/App.Commands.cs
@@ -37,10 +37,10 @@ namespace SourceGit
}
}
- public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
- public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
+ public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false));
+ public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false));
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
- public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
+ public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false));
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true));
public static readonly Command QuitCommand = new Command(_ => Quit(0));
public static readonly Command CopyTextBlockCommand = new Command(p =>
diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs
index f37e269c..9cad0792 100644
--- a/src/App.JsonCodeGen.cs
+++ b/src/App.JsonCodeGen.cs
@@ -46,8 +46,6 @@ namespace SourceGit
[JsonSerializable(typeof(Models.ExternalToolPaths))]
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
[JsonSerializable(typeof(Models.JetBrainsState))]
- [JsonSerializable(typeof(Models.OpenAIChatRequest))]
- [JsonSerializable(typeof(Models.OpenAIChatResponse))]
[JsonSerializable(typeof(Models.ThemeOverrides))]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.RepositorySettings))]
diff --git a/src/App.axaml b/src/App.axaml
index 76d4baa8..186022d5 100644
--- a/src/App.axaml
+++ b/src/App.axaml
@@ -16,10 +16,13 @@
+
+
+
@@ -32,7 +35,7 @@
-
+
diff --git a/src/App.axaml.cs b/src/App.axaml.cs
index cca9f2ea..935e2e19 100644
--- a/src/App.axaml.cs
+++ b/src/App.axaml.cs
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading;
using System.Threading.Tasks;
using Avalonia;
@@ -35,7 +38,6 @@ namespace SourceGit
TaskScheduler.UnobservedTaskException += (_, e) =>
{
- LogException(e.Exception);
e.SetObserved();
};
@@ -75,13 +77,72 @@ namespace SourceGit
Native.OS.SetupApp(builder);
return builder;
}
+
+ public static void LogException(Exception ex)
+ {
+ if (ex == null)
+ return;
+
+ var builder = new StringBuilder();
+ builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
+ builder.Append("----------------------------\n");
+ builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
+ builder.Append($"OS: {Environment.OSVersion}\n");
+ builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
+ builder.Append($"Source: {ex.Source}\n");
+ builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
+ builder.Append($"User: {Environment.UserName}\n");
+ builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
+ builder.Append($"Exception Time: {DateTime.Now}\n");
+ builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
+ builder.Append("---------------------------\n\n");
+ builder.Append(ex);
+
+ var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
+ var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
+ File.WriteAllText(file, builder.ToString());
+ }
#endregion
#region Utility Functions
- public static void OpenDialog(Window window)
+ public static void ShowWindow(object data, bool showAsDialog)
{
- if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
- window.ShowDialog(owner);
+ var impl = (Views.ChromelessWindow target, bool isDialog) =>
+ {
+ if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
+ {
+ if (isDialog)
+ target.ShowDialog(owner);
+ else
+ target.Show(owner);
+ }
+ else
+ {
+ target.Show();
+ }
+ };
+
+ if (data is Views.ChromelessWindow window)
+ {
+ impl(window, showAsDialog);
+ return;
+ }
+
+ var dataTypeName = data.GetType().FullName;
+ if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal))
+ return;
+
+ var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
+ var viewType = Type.GetType(viewTypeName);
+ if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow)))
+ return;
+
+ window = Activator.CreateInstance(viewType) as Views.ChromelessWindow;
+ if (window != null)
+ {
+ window.DataContext = data;
+ impl(window, showAsDialog);
+ }
}
public static void RaiseException(string context, string message)
@@ -179,6 +240,9 @@ namespace SourceGit
app._fontsOverrides = null;
}
+ defaultFont = app.FixFontFamilyName(defaultFont);
+ monospaceFont = app.FixFontFamilyName(monospaceFont);
+
var resDic = new ResourceDictionary();
if (!string.IsNullOrEmpty(defaultFont))
resDic.Add("Fonts.Default", new FontFamily(defaultFont));
@@ -237,7 +301,7 @@ namespace SourceGit
return await clipboard.GetTextAsync();
}
}
- return default;
+ return null;
}
public static string Text(string key, params object[] args)
@@ -259,8 +323,7 @@ namespace SourceGit
icon.Height = 12;
icon.Stretch = Stretch.Uniform;
- var geo = Current?.FindResource(key) as StreamGeometry;
- if (geo != null)
+ if (Current?.FindResource(key) is StreamGeometry geo)
icon.Data = geo;
return icon;
@@ -274,7 +337,7 @@ namespace SourceGit
return null;
}
- public static ViewModels.Launcher GetLauncer()
+ public static ViewModels.Launcher GetLauncher()
{
return Current is App app ? app._launcher : null;
}
@@ -312,43 +375,46 @@ namespace SourceGit
{
BindingPlugins.DataValidators.RemoveAt(0);
+ // Disable tooltip if window is not active.
+ ToolTip.ToolTipOpeningEvent.AddClassHandler((c, e) =>
+ {
+ var topLevel = TopLevel.GetTopLevel(c);
+ if (topLevel is not Window { IsActive: true })
+ e.Cancel = true;
+ });
+
if (TryLaunchAsCoreEditor(desktop))
return;
if (TryLaunchAsAskpass(desktop))
return;
- TryLaunchAsNormal(desktop);
+ _ipcChannel = new Models.IpcChannel();
+ if (!_ipcChannel.IsFirstInstance)
+ {
+ var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty;
+ if (!string.IsNullOrEmpty(arg))
+ {
+ if (arg.StartsWith('"') && arg.EndsWith('"'))
+ arg = arg.Substring(1, arg.Length - 2).Trim();
+
+ if (arg.Length > 0 && !Path.IsPathFullyQualified(arg))
+ arg = Path.GetFullPath(arg);
+ }
+
+ _ipcChannel.SendToFirstInstance(arg);
+ Environment.Exit(0);
+ }
+ else
+ {
+ _ipcChannel.MessageReceived += TryOpenRepository;
+ desktop.Exit += (_, _) => _ipcChannel.Dispose();
+ TryLaunchAsNormal(desktop);
+ }
}
}
#endregion
- private static void LogException(Exception ex)
- {
- if (ex == null)
- return;
-
- var builder = new StringBuilder();
- builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
- builder.Append("----------------------------\n");
- builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
- builder.Append($"OS: {Environment.OSVersion.ToString()}\n");
- builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
- builder.Append($"Source: {ex.Source}\n");
- builder.Append($"---------------------------\n\n");
- builder.Append(ex.StackTrace);
- while (ex.InnerException != null)
- {
- ex = ex.InnerException;
- builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n");
- builder.Append(ex.StackTrace);
- }
-
- var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
- var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
- File.WriteAllText(file, builder.ToString());
- }
-
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
{
exitCode = -1;
@@ -417,21 +483,37 @@ namespace SourceGit
return true;
var gitDir = Path.GetDirectoryName(file)!;
- var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json");
- if (!File.Exists(jobsFile))
- return true;
-
- var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection);
+ var origHeadFile = Path.Combine(gitDir, "rebase-merge", "orig-head");
+ var ontoFile = Path.Combine(gitDir, "rebase-merge", "onto");
var doneFile = Path.Combine(gitDir, "rebase-merge", "done");
- if (!File.Exists(doneFile))
+ var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json");
+ if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile))
return true;
- var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
- if (done.Length > collection.Jobs.Count)
+ var origHead = File.ReadAllText(origHeadFile).Trim();
+ var onto = File.ReadAllText(ontoFile).Trim();
+ var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection);
+ if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead))
return true;
- var job = collection.Jobs[done.Length - 1];
- File.WriteAllText(file, job.Message);
+ var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ if (done.Length == 0)
+ return true;
+
+ var current = done[^1].Trim();
+ var match = REG_REBASE_TODO().Match(current);
+ if (!match.Success)
+ return true;
+
+ var sha = match.Groups[1].Value;
+ foreach (var job in collection.Jobs)
+ {
+ if (job.SHA.StartsWith(sha))
+ {
+ File.WriteAllText(file, job.Message);
+ break;
+ }
+ }
return true;
}
@@ -439,15 +521,19 @@ namespace SourceGit
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{
var args = desktop.Args;
- if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
+ if (args is not { Length: > 1 } || !args[0].Equals("--core-editor", StringComparison.Ordinal))
return false;
var file = args[1];
if (!File.Exists(file))
+ {
desktop.Shutdown(-1);
- else
- desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
+ return true;
+ }
+ var editor = new Views.CommitMessageEditor();
+ editor.AsStandalone(file);
+ desktop.MainWindow = editor;
return true;
}
@@ -460,7 +546,9 @@ namespace SourceGit
var args = desktop.Args;
if (args?.Length > 0)
{
- desktop.MainWindow = new Views.Askpass(args[0]);
+ var askpass = new Views.Askpass();
+ askpass.TxtDescription.Text = args[0];
+ desktop.MainWindow = askpass;
return true;
}
@@ -469,23 +557,54 @@ namespace SourceGit
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{
- Native.OS.SetupEnternalTools();
+ Native.OS.SetupExternalTools();
Models.AvatarManager.Instance.Start();
string startupRepo = null;
if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]))
startupRepo = desktop.Args[0];
+ var pref = ViewModels.Preferences.Instance;
+ pref.SetCanModify();
+
_launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
+ desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
#if !DISABLE_UPDATE_DETECTION
- var pref = ViewModels.Preferences.Instance;
if (pref.ShouldCheck4UpdateOnStartup())
Check4Update();
#endif
}
+ private void TryOpenRepository(string repo)
+ {
+ if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo))
+ {
+ var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd();
+ if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
+ {
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false);
+ ViewModels.Welcome.Instance.Refresh();
+ _launcher?.OpenRepositoryInTab(node, null);
+
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd })
+ wnd.BringToTop();
+ });
+
+ return;
+ }
+ }
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher })
+ launcher.BringToTop();
+ });
+ }
+
private void Check4Update(bool manually = false)
{
Task.Run(async () =>
@@ -522,7 +641,7 @@ namespace SourceGit
catch (Exception e)
{
if (manually)
- ShowSelfUpdateResult(e);
+ ShowSelfUpdateResult(new Models.SelfUpdateFailed(e));
}
});
}
@@ -531,14 +650,54 @@ namespace SourceGit
{
Dispatcher.UIThread.Post(() =>
{
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
- {
- var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } };
- dialog.ShowDialog(owner);
- }
+ ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true);
});
}
+ private string FixFontFamilyName(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ return string.Empty;
+
+ var parts = input.Split(',');
+ var trimmed = new List();
+
+ foreach (var part in parts)
+ {
+ var t = part.Trim();
+ if (string.IsNullOrEmpty(t))
+ continue;
+
+ // Collapse multiple spaces into single space
+ var prevChar = '\0';
+ var sb = new StringBuilder();
+
+ foreach (var c in t)
+ {
+ if (c == ' ' && prevChar == ' ')
+ continue;
+ sb.Append(c);
+ prevChar = c;
+ }
+
+ var name = sb.ToString();
+ if (name.Contains('#', StringComparison.Ordinal))
+ {
+ if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) &&
+ !name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal))
+ continue;
+ }
+
+ trimmed.Add(name);
+ }
+
+ return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
+ }
+
+ [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")]
+ private static partial Regex REG_REBASE_TODO();
+
+ private Models.IpcChannel _ipcChannel = null;
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;
diff --git a/src/App.manifest b/src/App.manifest
index b3bc3bdf..11a2ff11 100644
--- a/src/App.manifest
+++ b/src/App.manifest
@@ -1,7 +1,7 @@
diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs
index e1b55b68..210eb4b2 100644
--- a/src/Commands/Add.cs
+++ b/src/Commands/Add.cs
@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using System.Text;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Add : Command
{
@@ -12,20 +9,11 @@ namespace SourceGit.Commands
Args = includeUntracked ? "add ." : "add -u .";
}
- public Add(string repo, List changes)
+ public Add(string repo, Models.Change change)
{
WorkingDirectory = repo;
Context = repo;
-
- var builder = new StringBuilder();
- builder.Append("add --");
- foreach (var c in changes)
- {
- builder.Append(" \"");
- builder.Append(c.Path);
- builder.Append("\"");
- }
- Args = builder.ToString();
+ Args = $"add -- \"{change.Path}\"";
}
public Add(string repo, string pathspecFromFile)
diff --git a/src/Commands/Archive.cs b/src/Commands/Archive.cs
index d4f6241c..5e0919f7 100644
--- a/src/Commands/Archive.cs
+++ b/src/Commands/Archive.cs
@@ -1,23 +1,12 @@
-using System;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Archive : Command
{
- public Archive(string repo, string revision, string saveTo, Action outputHandler)
+ public Archive(string repo, string revision, string saveTo)
{
WorkingDirectory = repo;
Context = repo;
Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}";
- TraitErrorAsOutput = true;
- _outputHandler = outputHandler;
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler;
}
}
diff --git a/src/Commands/AssumeUnchanged.cs b/src/Commands/AssumeUnchanged.cs
index 1898122a..28f78280 100644
--- a/src/Commands/AssumeUnchanged.cs
+++ b/src/Commands/AssumeUnchanged.cs
@@ -1,75 +1,14 @@
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
- public partial class AssumeUnchanged
+ public class AssumeUnchanged : Command
{
- [GeneratedRegex(@"^(\w)\s+(.+)$")]
- private static partial Regex REG_PARSE();
-
- class ViewCommand : Command
+ public AssumeUnchanged(string repo, string file, bool bAdd)
{
- public ViewCommand(string repo)
- {
- WorkingDirectory = repo;
- Args = "ls-files -v";
- RaiseError = false;
- }
+ var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
- public List Result()
- {
- Exec();
- return _outs;
- }
-
- protected override void OnReadline(string line)
- {
- var match = REG_PARSE().Match(line);
- if (!match.Success)
- return;
-
- if (match.Groups[1].Value == "h")
- {
- _outs.Add(match.Groups[2].Value);
- }
- }
-
- private readonly List _outs = new List();
+ WorkingDirectory = repo;
+ Context = repo;
+ Args = $"update-index {mode} -- \"{file}\"";
}
-
- class ModCommand : Command
- {
- public ModCommand(string repo, string file, bool bAdd)
- {
- var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
-
- WorkingDirectory = repo;
- Context = repo;
- Args = $"update-index {mode} -- \"{file}\"";
- }
- }
-
- public AssumeUnchanged(string repo)
- {
- _repo = repo;
- }
-
- public List View()
- {
- return new ViewCommand(_repo).Result();
- }
-
- public void Add(string file)
- {
- new ModCommand(_repo, file, true).Exec();
- }
-
- public void Remove(string file)
- {
- new ModCommand(_repo, file, false).Exec();
- }
-
- private readonly string _repo;
}
}
diff --git a/src/Commands/Bisect.cs b/src/Commands/Bisect.cs
new file mode 100644
index 00000000..a3bf1a97
--- /dev/null
+++ b/src/Commands/Bisect.cs
@@ -0,0 +1,13 @@
+namespace SourceGit.Commands
+{
+ public class Bisect : Command
+ {
+ public Bisect(string repo, string subcmd)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ RaiseError = false;
+ Args = $"bisect {subcmd}";
+ }
+ }
+}
diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs
index 291249be..1fc51fa4 100644
--- a/src/Commands/Blame.cs
+++ b/src/Commands/Blame.cs
@@ -21,10 +21,17 @@ namespace SourceGit.Commands
public Models.BlameData Result()
{
- var succ = Exec();
- if (!succ)
+ var rs = ReadToEnd();
+ if (!rs.IsSuccess)
+ return _result;
+
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
{
- return new Models.BlameData();
+ ParseLine(line);
+
+ if (_result.IsBinary)
+ break;
}
if (_needUnifyCommitSHA)
@@ -42,14 +49,9 @@ namespace SourceGit.Commands
return _result;
}
- protected override void OnReadline(string line)
+ private void ParseLine(string line)
{
- if (_result.IsBinary)
- return;
- if (string.IsNullOrEmpty(line))
- return;
-
- if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
+ if (line.Contains('\0', StringComparison.Ordinal))
{
_result.IsBinary = true;
_result.LineInfos.Clear();
@@ -87,7 +89,7 @@ namespace SourceGit.Commands
private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder();
- private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
+ private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly;
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;
diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs
index 391aeeb2..f207e976 100644
--- a/src/Commands/Branch.cs
+++ b/src/Commands/Branch.cs
@@ -1,4 +1,6 @@
-namespace SourceGit.Commands
+using System.Text;
+
+namespace SourceGit.Commands
{
public static class Branch
{
@@ -7,33 +9,44 @@
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
- cmd.Args = $"branch --show-current";
+ cmd.Args = "branch --show-current";
return cmd.ReadToEnd().StdOut.Trim();
}
- public static bool Create(string repo, string name, string basedOn)
+ public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log)
{
+ var builder = new StringBuilder();
+ builder.Append("branch ");
+ if (force)
+ builder.Append("-f ");
+ builder.Append(name);
+ builder.Append(" ");
+ builder.Append(basedOn);
+
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
- cmd.Args = $"branch {name} {basedOn}";
+ cmd.Args = builder.ToString();
+ cmd.Log = log;
return cmd.Exec();
}
- public static bool Rename(string repo, string name, string to)
+ public static bool Rename(string repo, string name, string to, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -M {name} {to}";
+ cmd.Log = log;
return cmd.Exec();
}
- public static bool SetUpstream(string repo, string name, string upstream)
+ public static bool SetUpstream(string repo, string name, string upstream, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
+ cmd.Log = log;
if (string.IsNullOrEmpty(upstream))
cmd.Args = $"branch {name} --unset-upstream";
@@ -43,32 +56,27 @@
return cmd.Exec();
}
- public static bool DeleteLocal(string repo, string name)
+ public static bool DeleteLocal(string repo, string name, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -D {name}";
+ cmd.Log = log;
return cmd.Exec();
}
- public static bool DeleteRemote(string repo, string remote, string name)
+ public static bool DeleteRemote(string repo, string remote, string name, Models.ICommandLog log)
{
+ bool exists = new Remote(repo).HasBranch(remote, name);
+ if (exists)
+ return new Push(repo, remote, $"refs/heads/{name}", true) { Log = log }.Exec();
+
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
-
- bool exists = new Remote(repo).HasBranch(remote, name);
- if (exists)
- {
- cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
- cmd.Args = $"push {remote} --delete {name}";
- }
- else
- {
- cmd.Args = $"branch -D -r {remote}/{name}";
- }
-
+ cmd.Args = $"branch -D -r {remote}/{name}";
+ cmd.Log = log;
return cmd.Exec();
}
}
diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs
index 306d62ff..d2876740 100644
--- a/src/Commands/Checkout.cs
+++ b/src/Commands/Checkout.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
@@ -12,19 +11,37 @@ namespace SourceGit.Commands
Context = repo;
}
- public bool Branch(string branch, Action onProgress)
+ public bool Branch(string branch, bool force)
{
- Args = $"checkout --recurse-submodules --progress {branch}";
- TraitErrorAsOutput = true;
- _outputHandler = onProgress;
+ var builder = new StringBuilder();
+ builder.Append("checkout --progress ");
+ if (force)
+ builder.Append("--force ");
+ builder.Append(branch);
+
+ Args = builder.ToString();
return Exec();
}
- public bool Branch(string branch, string basedOn, Action onProgress)
+ public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite)
{
- Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}";
- TraitErrorAsOutput = true;
- _outputHandler = onProgress;
+ var builder = new StringBuilder();
+ builder.Append("checkout --progress ");
+ if (force)
+ builder.Append("--force ");
+ builder.Append(allowOverwrite ? "-B " : "-b ");
+ builder.Append(branch);
+ builder.Append(" ");
+ builder.Append(basedOn);
+
+ Args = builder.ToString();
+ return Exec();
+ }
+
+ public bool Commit(string commitId, bool force)
+ {
+ var option = force ? "--force" : string.Empty;
+ Args = $"checkout {option} --detach --progress {commitId}";
return Exec();
}
@@ -61,20 +78,5 @@ namespace SourceGit.Commands
Args = $"checkout --no-overlay {revision} -- \"{file}\"";
return Exec();
}
-
- public bool Commit(string commitId, Action onProgress)
- {
- Args = $"checkout --detach --progress {commitId}";
- TraitErrorAsOutput = true;
- _outputHandler = onProgress;
- return Exec();
- }
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private Action _outputHandler;
}
}
diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs
index a10e5873..6ed74999 100644
--- a/src/Commands/Clean.cs
+++ b/src/Commands/Clean.cs
@@ -1,31 +1,12 @@
-using System.Collections.Generic;
-using System.Text;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Clean : Command
{
- public Clean(string repo, bool includeIgnored)
+ public Clean(string repo)
{
WorkingDirectory = repo;
Context = repo;
- Args = includeIgnored ? "clean -qfdx" : "clean -qfd";
- }
-
- public Clean(string repo, List files)
- {
- var builder = new StringBuilder();
- builder.Append("clean -qfd --");
- foreach (var f in files)
- {
- builder.Append(" \"");
- builder.Append(f);
- builder.Append("\"");
- }
-
- WorkingDirectory = repo;
- Context = repo;
- Args = builder.ToString();
+ Args = "clean -qfdx";
}
}
}
diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs
index 683b8846..efec264b 100644
--- a/src/Commands/Clone.cs
+++ b/src/Commands/Clone.cs
@@ -1,18 +1,13 @@
-using System;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Clone : Command
{
- private readonly Action _notifyProgress;
-
- public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action ouputHandler)
+ public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs)
{
Context = ctx;
WorkingDirectory = path;
- TraitErrorAsOutput = true;
SSHKey = sshKey;
- Args = "clone --progress --verbose --recurse-submodules ";
+ Args = "clone --progress --verbose ";
if (!string.IsNullOrEmpty(extraArgs))
Args += $"{extraArgs} ";
@@ -21,13 +16,6 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(localName))
Args += localName;
-
- _notifyProgress = ouputHandler;
- }
-
- protected override void OnReadline(string line)
- {
- _notifyProgress?.Invoke(line);
}
}
}
diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs
index 3f61de17..a5ec0b56 100644
--- a/src/Commands/Command.cs
+++ b/src/Commands/Command.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
+using System.Threading;
using Avalonia.Threading;
@@ -10,11 +11,6 @@ namespace SourceGit.Commands
{
public partial class Command
{
- public class CancelToken
- {
- public bool Requested { get; set; } = false;
- }
-
public class ReadToEndResult
{
public bool IsSuccess { get; set; } = false;
@@ -30,82 +26,51 @@ namespace SourceGit.Commands
}
public string Context { get; set; } = string.Empty;
- public CancelToken Cancel { get; set; } = null;
+ public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
public string WorkingDirectory { get; set; } = null;
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
public string SSHKey { get; set; } = string.Empty;
public string Args { get; set; } = string.Empty;
public bool RaiseError { get; set; } = true;
- public bool TraitErrorAsOutput { get; set; } = false;
+ public Models.ICommandLog Log { get; set; } = null;
public bool Exec()
{
+ Log?.AppendLine($"$ git {Args}\n");
+
var start = CreateGitStartInfo();
var errs = new List();
var proc = new Process() { StartInfo = start };
- var isCancelled = false;
- proc.OutputDataReceived += (_, e) =>
- {
- if (Cancel != null && Cancel.Requested)
- {
- isCancelled = true;
- proc.CancelErrorRead();
- proc.CancelOutputRead();
- if (!proc.HasExited)
- proc.Kill(true);
- return;
- }
-
- if (e.Data != null)
- OnReadline(e.Data);
- };
-
- proc.ErrorDataReceived += (_, e) =>
- {
- if (Cancel != null && Cancel.Requested)
- {
- isCancelled = true;
- proc.CancelErrorRead();
- proc.CancelOutputRead();
- if (!proc.HasExited)
- proc.Kill(true);
- return;
- }
-
- if (string.IsNullOrEmpty(e.Data))
- {
- errs.Add(string.Empty);
- return;
- }
-
- if (TraitErrorAsOutput)
- OnReadline(e.Data);
-
- // Ignore progress messages
- if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal))
- return;
- if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal))
- return;
- if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal))
- return;
- if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal))
- return;
- if (REG_PROGRESS().IsMatch(e.Data))
- return;
-
- errs.Add(e.Data);
- };
+ proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs);
+ proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs);
+ var dummy = null as Process;
+ var dummyProcLock = new object();
try
{
proc.Start();
+
+ // It not safe, please only use `CancellationToken` in readonly commands.
+ if (CancellationToken.CanBeCanceled)
+ {
+ dummy = proc;
+ CancellationToken.Register(() =>
+ {
+ lock (dummyProcLock)
+ {
+ if (dummy is { HasExited: false })
+ dummy.Kill();
+ }
+ });
+ }
}
catch (Exception e)
{
if (RaiseError)
Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message));
+ Log?.AppendLine(string.Empty);
return false;
}
@@ -113,10 +78,19 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine();
proc.WaitForExit();
+ if (dummy != null)
+ {
+ lock (dummyProcLock)
+ {
+ dummy = null;
+ }
+ }
+
int exitCode = proc.ExitCode;
proc.Close();
+ Log?.AppendLine(string.Empty);
- if (!isCancelled && exitCode != 0)
+ if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
if (RaiseError)
{
@@ -163,11 +137,6 @@ namespace SourceGit.Commands
return rs;
}
- protected virtual void OnReadline(string line)
- {
- // Implemented by derived class
- }
-
private ProcessStartInfo CreateGitStartInfo()
{
var start = new ProcessStartInfo();
@@ -192,27 +161,20 @@ namespace SourceGit.Commands
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
- // Force using en_US.UTF-8 locale to avoid GCM crash
+ // Force using en_US.UTF-8 locale
if (OperatingSystem.IsLinux())
- start.Environment.Add("LANG", "en_US.UTF-8");
-
- // Fix macOS `PATH` env
- if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
- start.Environment.Add("PATH", Native.OS.CustomPathEnv);
+ {
+ start.Environment.Add("LANG", "C");
+ start.Environment.Add("LC_ALL", "C");
+ }
// Force using this app as git editor.
- switch (Editor)
+ start.Arguments += Editor switch
{
- case EditorType.CoreEditor:
- start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" ";
- break;
- case EditorType.RebaseEditor:
- start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true ";
- break;
- default:
- start.Arguments += "-c core.editor=true ";
- break;
- }
+ EditorType.CoreEditor => $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" ",
+ EditorType.RebaseEditor => $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true ",
+ _ => "-c core.editor=true ",
+ };
// Append command args
start.Arguments += Args;
@@ -224,6 +186,28 @@ namespace SourceGit.Commands
return start;
}
+ private void HandleOutput(string line, List errs)
+ {
+ line ??= string.Empty;
+ Log?.AppendLine(line);
+
+ // Lines to hide in error message.
+ if (line.Length > 0)
+ {
+ if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) ||
+ line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) ||
+ line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) ||
+ line.StartsWith("Filtering content:", StringComparison.Ordinal) ||
+ line.StartsWith("hint:", StringComparison.Ordinal))
+ return;
+
+ if (REG_PROGRESS().IsMatch(line))
+ return;
+ }
+
+ errs.Add(line);
+ }
+
[GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS();
}
diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs
index cb086793..1585e7e3 100644
--- a/src/Commands/Commit.cs
+++ b/src/Commands/Commit.cs
@@ -4,19 +4,18 @@ namespace SourceGit.Commands
{
public class Commit : Command
{
- public Commit(string repo, string message, bool amend, bool signOff)
+ public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor)
{
_tmpFile = Path.GetTempFileName();
File.WriteAllText(_tmpFile, message);
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
Args = $"commit --allow-empty --file=\"{_tmpFile}\"";
- if (amend)
- Args += " --amend --no-edit";
if (signOff)
Args += " --signoff";
+ if (amend)
+ Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit";
}
public bool Run()
@@ -35,6 +34,6 @@ namespace SourceGit.Commands
return succ;
}
- private string _tmpFile = string.Empty;
+ private readonly string _tmpFile;
}
}
diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs
index c4674c8e..c88e087a 100644
--- a/src/Commands/CompareRevisions.cs
+++ b/src/Commands/CompareRevisions.cs
@@ -6,8 +6,10 @@ namespace SourceGit.Commands
{
public partial class CompareRevisions : Command
{
- [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
+ [GeneratedRegex(@"^([MADC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
+ [GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
+ private static partial Regex REG_RENAME_FORMAT();
public CompareRevisions(string repo, string start, string end)
{
@@ -18,18 +20,44 @@ namespace SourceGit.Commands
Args = $"diff --name-status {based} {end}";
}
+ public CompareRevisions(string repo, string start, string end, string path)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+
+ var based = string.IsNullOrEmpty(start) ? "-R" : start;
+ Args = $"diff --name-status {based} {end} -- \"{path}\"";
+ }
+
public List Result()
{
- Exec();
- _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
+ var rs = ReadToEnd();
+ if (!rs.IsSuccess)
+ return _changes;
+
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ ParseLine(line);
+
+ _changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path));
return _changes;
}
- protected override void OnReadline(string line)
+ private void ParseLine(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
+ {
+ match = REG_RENAME_FORMAT().Match(line);
+ if (match.Success)
+ {
+ var renamed = new Models.Change() { Path = match.Groups[1].Value };
+ renamed.Set(Models.ChangeState.Renamed);
+ _changes.Add(renamed);
+ }
+
return;
+ }
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
@@ -48,10 +76,6 @@ namespace SourceGit.Commands
change.Set(Models.ChangeState.Deleted);
_changes.Add(change);
break;
- case 'R':
- change.Set(Models.ChangeState.Renamed);
- _changes.Add(change);
- break;
case 'C':
change.Set(Models.ChangeState.Copied);
_changes.Add(change);
diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs
index 2fb83325..49e8fcb7 100644
--- a/src/Commands/Config.cs
+++ b/src/Commands/Config.cs
@@ -29,7 +29,7 @@ namespace SourceGit.Commands
var rs = new Dictionary();
if (output.IsSuccess)
{
- var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var idx = line.IndexOf('=', StringComparison.Ordinal);
diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChangesWithoutUntracked.cs
index afb62840..a704f313 100644
--- a/src/Commands/CountLocalChangesWithoutUntracked.cs
+++ b/src/Commands/CountLocalChangesWithoutUntracked.cs
@@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = "status -uno --ignore-submodules=dirty --porcelain";
+ Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain";
}
public int Result()
@@ -16,7 +16,7 @@ namespace SourceGit.Commands
var rs = ReadToEnd();
if (rs.IsSuccess)
{
- var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
return lines.Length;
}
diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs
index da971e58..6af0a3cc 100644
--- a/src/Commands/Diff.cs
+++ b/src/Commands/Diff.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@@ -28,34 +28,48 @@ namespace SourceGit.Commands
Context = repo;
if (ignoreWhitespace)
- Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
+ Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}";
+ else if (Models.DiffOption.IgnoreCRAtEOL)
+ Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
else
- Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
+ Args = $"diff --no-ext-diff --patch --unified={unified} {opt}";
}
public Models.DiffResult Result()
{
- Exec();
+ var rs = ReadToEnd();
+ var start = 0;
+ var end = rs.StdOut.IndexOf('\n', start);
+ while (end > 0)
+ {
+ var line = rs.StdOut.Substring(start, end - start);
+ ParseLine(line);
- if (_result.IsBinary || _result.IsLFS)
+ start = end + 1;
+ end = rs.StdOut.IndexOf('\n', start);
+ }
+
+ if (start < rs.StdOut.Length)
+ ParseLine(rs.StdOut.Substring(start));
+
+ if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0)
{
_result.TextDiff = null;
}
else
{
ProcessInlineHighlights();
-
- if (_result.TextDiff.Lines.Count == 0)
- _result.TextDiff = null;
- else
- _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
+ _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
return _result;
}
- protected override void OnReadline(string line)
+ private void ParseLine(string line)
{
+ if (_result.IsBinary)
+ return;
+
if (line.StartsWith("old mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(9);
@@ -68,8 +82,17 @@ namespace SourceGit.Commands
return;
}
- if (_result.IsBinary)
+ if (line.StartsWith("deleted file mode ", StringComparison.Ordinal))
+ {
+ _result.OldMode = line.Substring(18);
return;
+ }
+
+ if (line.StartsWith("new file mode ", StringComparison.Ordinal))
+ {
+ _result.NewMode = line.Substring(14);
+ return;
+ }
if (_result.IsLFS)
{
@@ -82,7 +105,7 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("-size ", StringComparison.Ordinal))
{
- _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
+ _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
}
}
else if (ch == '+')
@@ -93,12 +116,12 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
- _result.LFSDiff.New.Size = long.Parse(line.Substring(6));
+ _result.LFSDiff.New.Size = long.Parse(line.AsSpan(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
- _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
+ _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
}
return;
}
@@ -128,7 +151,8 @@ namespace SourceGit.Commands
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
- _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
+ _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0);
+ _result.TextDiff.Lines.Add(_last);
}
}
else
@@ -136,7 +160,8 @@ namespace SourceGit.Commands
if (line.Length == 0)
{
ProcessInlineHighlights();
- _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
+ _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine);
+ _result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
return;
@@ -152,7 +177,8 @@ namespace SourceGit.Commands
return;
}
- _deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
+ _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0);
+ _deleted.Add(_last);
_oldLine++;
}
else if (ch == '+')
@@ -164,7 +190,8 @@ namespace SourceGit.Commands
return;
}
- _added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
+ _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine);
+ _added.Add(_last);
_newLine++;
}
else if (ch != '\\')
@@ -175,7 +202,8 @@ namespace SourceGit.Commands
{
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
- _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
+ _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0);
+ _result.TextDiff.Lines.Add(_last);
}
else
{
@@ -186,11 +214,16 @@ namespace SourceGit.Commands
return;
}
- _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine));
+ _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine);
+ _result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
}
}
+ else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal))
+ {
+ _last.NoNewLineEndOfFile = true;
+ }
}
}
@@ -241,6 +274,7 @@ namespace SourceGit.Commands
private readonly Models.DiffResult _result = new Models.DiffResult();
private readonly List _deleted = new List();
private readonly List _added = new List();
+ private Models.TextDiffLine _last = null;
private int _oldLine = 0;
private int _newLine = 0;
}
diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs
index a279bb84..f36ca6c9 100644
--- a/src/Commands/Discard.cs
+++ b/src/Commands/Discard.cs
@@ -1,39 +1,95 @@
using System;
using System.Collections.Generic;
+using System.IO;
+
+using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class Discard
{
- public static void All(string repo, bool includeIgnored)
+ ///
+ /// Discard all local changes (unstaged & staged)
+ ///
+ ///
+ ///
+ ///
+ public static void All(string repo, bool includeIgnored, Models.ICommandLog log)
{
- new Restore(repo).Exec();
- new Clean(repo, includeIgnored).Exec();
+ var changes = new QueryLocalChanges(repo).Result();
+ try
+ {
+ foreach (var c in changes)
+ {
+ if (c.WorkTree == Models.ChangeState.Untracked ||
+ c.WorkTree == Models.ChangeState.Added ||
+ c.Index == Models.ChangeState.Added ||
+ c.Index == Models.ChangeState.Renamed)
+ {
+ var fullPath = Path.Combine(repo, c.Path);
+ if (Directory.Exists(fullPath))
+ Directory.Delete(fullPath, true);
+ else
+ File.Delete(fullPath);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}");
+ });
+ }
+
+ new Reset(repo, "HEAD", "--hard") { Log = log }.Exec();
+
+ if (includeIgnored)
+ new Clean(repo) { Log = log }.Exec();
}
- public static void Changes(string repo, List changes)
+ ///
+ /// Discard selected changes (only unstaged).
+ ///
+ ///
+ ///
+ ///
+ public static void Changes(string repo, List changes, Models.ICommandLog log)
{
- var needClean = new List();
- var needCheckout = new List();
+ var restores = new List();
- foreach (var c in changes)
+ try
{
- if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
- needClean.Add(c.Path);
- else
- needCheckout.Add(c.Path);
+ foreach (var c in changes)
+ {
+ if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
+ {
+ var fullPath = Path.Combine(repo, c.Path);
+ if (Directory.Exists(fullPath))
+ Directory.Delete(fullPath, true);
+ else
+ File.Delete(fullPath);
+ }
+ else
+ {
+ restores.Add(c.Path);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}");
+ });
}
- for (int i = 0; i < needClean.Count; i += 10)
+ if (restores.Count > 0)
{
- var count = Math.Min(10, needClean.Count - i);
- new Clean(repo, needClean.GetRange(i, count)).Exec();
- }
-
- for (int i = 0; i < needCheckout.Count; i += 10)
- {
- var count = Math.Min(10, needCheckout.Count - i);
- new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules").Exec();
+ var pathSpecFile = Path.GetTempFileName();
+ File.WriteAllLines(pathSpecFile, restores);
+ new Restore(repo, pathSpecFile, false) { Log = log }.Exec();
+ File.Delete(pathSpecFile);
}
}
}
diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs
deleted file mode 100644
index a10f5387..00000000
--- a/src/Commands/ExecuteCustomAction.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Text;
-
-using Avalonia.Threading;
-
-namespace SourceGit.Commands
-{
- public static class ExecuteCustomAction
- {
- public static void Run(string repo, string file, string args, Action outputHandler)
- {
- var start = new ProcessStartInfo();
- start.FileName = file;
- start.Arguments = args;
- start.UseShellExecute = false;
- start.CreateNoWindow = true;
- start.RedirectStandardOutput = true;
- start.RedirectStandardError = true;
- start.StandardOutputEncoding = Encoding.UTF8;
- start.StandardErrorEncoding = Encoding.UTF8;
- start.WorkingDirectory = repo;
-
- // Force using en_US.UTF-8 locale to avoid GCM crash
- if (OperatingSystem.IsLinux())
- start.Environment.Add("LANG", "en_US.UTF-8");
-
- // Fix macOS `PATH` env
- if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
- start.Environment.Add("PATH", Native.OS.CustomPathEnv);
-
- var proc = new Process() { StartInfo = start };
- var builder = new StringBuilder();
-
- proc.OutputDataReceived += (_, e) =>
- {
- if (e.Data != null)
- outputHandler?.Invoke(e.Data);
- };
-
- proc.ErrorDataReceived += (_, e) =>
- {
- if (e.Data != null)
- {
- outputHandler?.Invoke(e.Data);
- builder.AppendLine(e.Data);
- }
- };
-
- try
- {
- proc.Start();
- proc.BeginOutputReadLine();
- proc.BeginErrorReadLine();
- proc.WaitForExit();
-
- var exitCode = proc.ExitCode;
- if (exitCode != 0)
- {
- var errMsg = builder.ToString();
- Dispatcher.UIThread.Invoke(() =>
- {
- App.RaiseException(repo, errMsg);
- });
- }
- }
- catch (Exception e)
- {
- Dispatcher.UIThread.Invoke(() =>
- {
- App.RaiseException(repo, e.Message);
- });
- }
-
- proc.Close();
- }
- }
-}
diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs
index 1c3e78cb..edf2a6dd 100644
--- a/src/Commands/Fetch.cs
+++ b/src/Commands/Fetch.cs
@@ -1,15 +1,11 @@
-using System;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Fetch : Command
{
- public Fetch(string repo, string remote, bool noTags, bool prune, bool force, Action outputHandler)
+ public Fetch(string repo, string remote, bool noTags, bool force)
{
- _outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "fetch --progress --verbose ";
@@ -21,27 +17,15 @@ namespace SourceGit.Commands
if (force)
Args += "--force ";
- if (prune)
- Args += "--prune ";
-
Args += remote;
}
- public Fetch(string repo, Models.Branch local, Models.Branch remote, Action outputHandler)
+ public Fetch(string repo, Models.Branch local, Models.Branch remote)
{
- _outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey");
Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}";
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler;
}
}
diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs
index b3ec2e4a..bf850d60 100644
--- a/src/Commands/FormatPatch.cs
+++ b/src/Commands/FormatPatch.cs
@@ -6,6 +6,7 @@
{
WorkingDirectory = repo;
Context = repo;
+ Editor = EditorType.None;
Args = $"format-patch {commit} -1 --output=\"{saveTo}\"";
}
}
diff --git a/src/Commands/GC.cs b/src/Commands/GC.cs
index 393b915e..0b27f487 100644
--- a/src/Commands/GC.cs
+++ b/src/Commands/GC.cs
@@ -1,23 +1,12 @@
-using System;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class GC : Command
{
- public GC(string repo, Action outputHandler)
+ public GC(string repo)
{
- _outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
Args = "gc --prune=now";
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler;
}
}
diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs
index e4f25f38..df61fdd2 100644
--- a/src/Commands/GenerateCommitMessage.cs
+++ b/src/Commands/GenerateCommitMessage.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Text;
using System.Threading;
+using Avalonia.Threading;
+
namespace SourceGit.Commands
{
///
@@ -20,82 +22,78 @@ namespace SourceGit.Commands
}
}
- public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onProgress)
+ public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onResponse)
{
_service = service;
_repo = repo;
_changes = changes;
_cancelToken = cancelToken;
- _onProgress = onProgress;
+ _onResponse = onResponse;
}
- public string Result()
+ public void Exec()
{
try
{
- var summarybuilder = new StringBuilder();
- var bodyBuilder = new StringBuilder();
+ _onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
+
+ var responseBuilder = new StringBuilder();
+ var summaryBuilder = new StringBuilder();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
- return "";
+ return;
- _onProgress?.Invoke($"Analyzing {change.Path}...");
+ responseBuilder.Append("- ");
+ summaryBuilder.Append("- ");
- var summary = GenerateChangeSummary(change);
- summarybuilder.Append("- ");
- summarybuilder.Append(summary);
- summarybuilder.Append("(file: ");
- summarybuilder.Append(change.Path);
- summarybuilder.Append(")");
- summarybuilder.AppendLine();
+ var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
+ if (rs.IsSuccess)
+ {
+ _service.Chat(
+ _service.AnalyzeDiffPrompt,
+ $"Here is the `git diff` output: {rs.StdOut}",
+ _cancelToken,
+ update =>
+ {
+ responseBuilder.Append(update);
+ summaryBuilder.Append(update);
- bodyBuilder.Append("- ");
- bodyBuilder.Append(summary);
- bodyBuilder.AppendLine();
+ _onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}");
+ });
+ }
+
+ responseBuilder.Append("\n");
+ summaryBuilder.Append("(file: ");
+ summaryBuilder.Append(change.Path);
+ summaryBuilder.Append(")\n");
}
if (_cancelToken.IsCancellationRequested)
- return "";
+ return;
- _onProgress?.Invoke($"Generating commit message...");
-
- var body = bodyBuilder.ToString();
- var subject = GenerateSubject(summarybuilder.ToString());
- return string.Format("{0}\n\n{1}", subject, body);
+ var responseBody = responseBuilder.ToString();
+ var subjectBuilder = new StringBuilder();
+ _service.Chat(
+ _service.GenerateSubjectPrompt,
+ $"Here are the summaries changes:\n{summaryBuilder}",
+ _cancelToken,
+ update =>
+ {
+ subjectBuilder.Append(update);
+ _onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
+ });
}
catch (Exception e)
{
- App.RaiseException(_repo, $"Failed to generate commit message: {e}");
- return "";
+ Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
}
}
- private string GenerateChangeSummary(Models.Change change)
- {
- var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
- var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
-
- var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
- if (rsp != null && rsp.Choices.Count > 0)
- return rsp.Choices[0].Message.Content;
-
- return string.Empty;
- }
-
- private string GenerateSubject(string summary)
- {
- var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
- if (rsp != null && rsp.Choices.Count > 0)
- return rsp.Choices[0].Message.Content;
-
- return string.Empty;
- }
-
private Models.OpenAIService _service;
private string _repo;
private List _changes;
private CancellationToken _cancelToken;
- private Action _onProgress;
+ private Action _onResponse;
}
}
diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs
index 5e05ed83..1d33fa3a 100644
--- a/src/Commands/GitFlow.cs
+++ b/src/Commands/GitFlow.cs
@@ -1,52 +1,12 @@
-using System;
-using System.Collections.Generic;
-
+using System.Text;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class GitFlow
{
- public class BranchDetectResult
+ public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log)
{
- public bool IsGitFlowBranch { get; set; } = false;
- public string Type { get; set; } = string.Empty;
- public string Prefix { get; set; } = string.Empty;
- }
-
- public static bool IsEnabled(string repo, List branches)
- {
- var localBrancheNames = new HashSet();
- foreach (var branch in branches)
- {
- if (branch.IsLocal)
- localBrancheNames.Add(branch.Name);
- }
-
- var config = new Config(repo).ListAll();
- if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
- return false;
-
- if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop))
- return false;
-
- return config.ContainsKey("gitflow.prefix.feature") &&
- config.ContainsKey("gitflow.prefix.release") &&
- config.ContainsKey("gitflow.prefix.hotfix");
- }
-
- public static bool Init(string repo, List branches, string master, string develop, string feature, string release, string hotfix, string version)
- {
- var current = branches.Find(x => x.IsCurrent);
-
- var masterBranch = branches.Find(x => x.Name == master);
- if (masterBranch == null && current != null)
- Branch.Create(repo, master, current.Head);
-
- var devBranch = branches.Find(x => x.Name == develop);
- if (devBranch == null && current != null)
- Branch.Create(repo, develop, current.Head);
-
var config = new Config(repo);
config.Set("gitflow.branch.master", master);
config.Set("gitflow.branch.develop", develop);
@@ -61,104 +21,72 @@ namespace SourceGit.Commands
init.WorkingDirectory = repo;
init.Context = repo;
init.Args = "flow init -d";
+ init.Log = log;
return init.Exec();
}
- public static string GetPrefix(string repo, string type)
+ public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log)
{
- return new Config(repo).Get($"gitflow.prefix.{type}");
- }
-
- public static BranchDetectResult DetectType(string repo, List branches, string branch)
- {
- var rs = new BranchDetectResult();
- var localBrancheNames = new HashSet();
- foreach (var b in branches)
- {
- if (b.IsLocal)
- localBrancheNames.Add(b.Name);
- }
-
- var config = new Config(repo).ListAll();
- if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
- return rs;
-
- if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop))
- return rs;
-
- if (!config.TryGetValue("gitflow.prefix.feature", out var feature) ||
- !config.TryGetValue("gitflow.prefix.release", out var release) ||
- !config.TryGetValue("gitflow.prefix.hotfix", out var hotfix))
- return rs;
-
- if (branch.StartsWith(feature, StringComparison.Ordinal))
- {
- rs.IsGitFlowBranch = true;
- rs.Type = "feature";
- rs.Prefix = feature;
- }
- else if (branch.StartsWith(release, StringComparison.Ordinal))
- {
- rs.IsGitFlowBranch = true;
- rs.Type = "release";
- rs.Prefix = release;
- }
- else if (branch.StartsWith(hotfix, StringComparison.Ordinal))
- {
- rs.IsGitFlowBranch = true;
- rs.Type = "hotfix";
- rs.Prefix = hotfix;
- }
-
- return rs;
- }
-
- public static bool Start(string repo, string type, string name)
- {
- if (!SUPPORTED_BRANCH_TYPES.Contains(type))
- {
- Dispatcher.UIThread.Post(() =>
- {
- App.RaiseException(repo, "Bad branch type!!!");
- });
-
- return false;
- }
-
var start = new Command();
start.WorkingDirectory = repo;
start.Context = repo;
- start.Args = $"flow {type} start {name}";
+
+ switch (type)
+ {
+ case Models.GitFlowBranchType.Feature:
+ start.Args = $"flow feature start {name}";
+ break;
+ case Models.GitFlowBranchType.Release:
+ start.Args = $"flow release start {name}";
+ break;
+ case Models.GitFlowBranchType.Hotfix:
+ start.Args = $"flow hotfix start {name}";
+ break;
+ default:
+ Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
+ return false;
+ }
+
+ start.Log = log;
return start.Exec();
}
- public static bool Finish(string repo, string type, string name, bool keepBranch)
+ public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log)
{
- if (!SUPPORTED_BRANCH_TYPES.Contains(type))
- {
- Dispatcher.UIThread.Post(() =>
- {
- App.RaiseException(repo, "Bad branch type!!!");
- });
+ var builder = new StringBuilder();
+ builder.Append("flow ");
- return false;
+ switch (type)
+ {
+ case Models.GitFlowBranchType.Feature:
+ builder.Append("feature");
+ break;
+ case Models.GitFlowBranchType.Release:
+ builder.Append("release");
+ break;
+ case Models.GitFlowBranchType.Hotfix:
+ builder.Append("hotfix");
+ break;
+ default:
+ Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
+ return false;
}
- var option = keepBranch ? "-k" : string.Empty;
+ builder.Append(" finish ");
+ if (squash)
+ builder.Append("--squash ");
+ if (push)
+ builder.Append("--push ");
+ if (keepBranch)
+ builder.Append("-k ");
+ builder.Append(name);
+
var finish = new Command();
finish.WorkingDirectory = repo;
finish.Context = repo;
- finish.Args = $"flow {type} finish {option} {name}";
+ finish.Args = builder.ToString();
+ finish.Log = log;
return finish.Exec();
}
-
- private static readonly List SUPPORTED_BRANCH_TYPES = new List()
- {
- "feature",
- "release",
- "bugfix",
- "hotfix",
- "support",
- };
}
}
diff --git a/src/Commands/GitIgnore.cs b/src/Commands/GitIgnore.cs
deleted file mode 100644
index e666eba6..00000000
--- a/src/Commands/GitIgnore.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.IO;
-
-namespace SourceGit.Commands
-{
- public static class GitIgnore
- {
- public static void Add(string repo, string pattern)
- {
- var file = Path.Combine(repo, ".gitignore");
- if (!File.Exists(file))
- File.WriteAllLines(file, [pattern]);
- else
- File.AppendAllLines(file, [pattern]);
- }
- }
-}
diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs
index de59b5a4..af8f54bb 100644
--- a/src/Commands/IsBinary.cs
+++ b/src/Commands/IsBinary.cs
@@ -11,7 +11,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
+ Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- \"{path}\"";
RaiseError = false;
}
diff --git a/src/Commands/IsCommitSHA.cs b/src/Commands/IsCommitSHA.cs
new file mode 100644
index 00000000..1b0c50e3
--- /dev/null
+++ b/src/Commands/IsCommitSHA.cs
@@ -0,0 +1,17 @@
+namespace SourceGit.Commands
+{
+ public class IsCommitSHA : Command
+ {
+ public IsCommitSHA(string repo, string hash)
+ {
+ WorkingDirectory = repo;
+ Args = $"cat-file -t {hash}";
+ }
+
+ public bool Result()
+ {
+ var rs = ReadToEnd();
+ return rs.IsSuccess && rs.StdOut.Trim().Equals("commit");
+ }
+ }
+}
diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs
index 13bc12ac..9b243451 100644
--- a/src/Commands/IsConflictResolved.cs
+++ b/src/Commands/IsConflictResolved.cs
@@ -10,5 +10,10 @@
Context = repo;
Args = $"diff -a --ignore-cr-at-eol --check {opt}";
}
+
+ public bool Result()
+ {
+ return ReadToEnd().IsSuccess;
+ }
}
}
diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs
index 2b7d1de4..18d2ba93 100644
--- a/src/Commands/LFS.cs
+++ b/src/Commands/LFS.cs
@@ -10,23 +10,15 @@ namespace SourceGit.Commands
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK();
- class SubCmd : Command
+ private class SubCmd : Command
{
- public SubCmd(string repo, string args, Action onProgress)
+ public SubCmd(string repo, string args, Models.ICommandLog log)
{
WorkingDirectory = repo;
Context = repo;
Args = args;
- TraitErrorAsOutput = true;
- _outputHandler = onProgress;
+ Log = log;
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler;
}
public LFS(string repo)
@@ -44,35 +36,35 @@ namespace SourceGit.Commands
return content.Contains("git lfs pre-push");
}
- public bool Install()
+ public bool Install(Models.ICommandLog log)
{
- return new SubCmd(_repo, "lfs install --local", null).Exec();
+ return new SubCmd(_repo, "lfs install --local", log).Exec();
}
- public bool Track(string pattern, bool isFilenameMode = false)
+ public bool Track(string pattern, bool isFilenameMode, Models.ICommandLog log)
{
var opt = isFilenameMode ? "--filename" : "";
- return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", null).Exec();
+ return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", log).Exec();
}
- public void Fetch(string remote, Action outputHandler)
+ public void Fetch(string remote, Models.ICommandLog log)
{
- new SubCmd(_repo, $"lfs fetch {remote}", outputHandler).Exec();
+ new SubCmd(_repo, $"lfs fetch {remote}", log).Exec();
}
- public void Pull(string remote, Action outputHandler)
+ public void Pull(string remote, Models.ICommandLog log)
{
- new SubCmd(_repo, $"lfs pull {remote}", outputHandler).Exec();
+ new SubCmd(_repo, $"lfs pull {remote}", log).Exec();
}
- public void Push(string remote, Action outputHandler)
+ public void Push(string remote, Models.ICommandLog log)
{
- new SubCmd(_repo, $"lfs push {remote}", outputHandler).Exec();
+ new SubCmd(_repo, $"lfs push {remote}", log).Exec();
}
- public void Prune(Action outputHandler)
+ public void Prune(Models.ICommandLog log)
{
- new SubCmd(_repo, "lfs prune", outputHandler).Exec();
+ new SubCmd(_repo, "lfs prune", log).Exec();
}
public List Locks(string remote)
@@ -82,7 +74,7 @@ namespace SourceGit.Commands
var rs = cmd.ReadToEnd();
if (rs.IsSuccess)
{
- var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_LOCK().Match(line);
@@ -101,21 +93,21 @@ namespace SourceGit.Commands
return locks;
}
- public bool Lock(string remote, string file)
+ public bool Lock(string remote, string file, Models.ICommandLog log)
{
- return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", null).Exec();
+ return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", log).Exec();
}
- public bool Unlock(string remote, string file, bool force)
+ public bool Unlock(string remote, string file, bool force, Models.ICommandLog log)
{
var opt = force ? "-f" : "";
- return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", null).Exec();
+ return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", log).Exec();
}
- public bool Unlock(string remote, long id, bool force)
+ public bool Unlock(string remote, long id, bool force, Models.ICommandLog log)
{
var opt = force ? "-f" : "";
- return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", null).Exec();
+ return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", log).Exec();
}
private readonly string _repo;
diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs
index bd1f3653..32898593 100644
--- a/src/Commands/Merge.cs
+++ b/src/Commands/Merge.cs
@@ -1,26 +1,30 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Merge : Command
{
- public Merge(string repo, string source, string mode, Action outputHandler)
+ public Merge(string repo, string source, string mode, bool edit)
{
- _outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
- Args = $"merge --progress {source} {mode}";
+ Editor = EditorType.CoreEditor;
+
+ var builder = new StringBuilder();
+ builder.Append("merge --progress ");
+ builder.Append(edit ? "--edit " : "--no-edit ");
+ builder.Append(source);
+ builder.Append(' ');
+ builder.Append(mode);
+
+ Args = builder.ToString();
}
- public Merge(string repo, List targets, bool autoCommit, string strategy, Action outputHandler)
+ public Merge(string repo, List targets, bool autoCommit, string strategy)
{
- _outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
var builder = new StringBuilder();
builder.Append("merge --progress ");
@@ -37,12 +41,5 @@ namespace SourceGit.Commands
Args = builder.ToString();
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler = null;
}
}
diff --git a/src/Commands/MergeTool.cs b/src/Commands/MergeTool.cs
index d3478d59..fc6d0d75 100644
--- a/src/Commands/MergeTool.cs
+++ b/src/Commands/MergeTool.cs
@@ -13,15 +13,18 @@ namespace SourceGit.Commands
cmd.Context = repo;
cmd.RaiseError = true;
+ // NOTE: If no names are specified, 'git mergetool' will run the merge tool program on every file with merge conflicts.
+ var fileArg = string.IsNullOrEmpty(file) ? "" : $"\"{file}\"";
+
if (toolType == 0)
{
- cmd.Args = $"mergetool \"{file}\"";
+ cmd.Args = $"mergetool {fileArg}";
return cmd.Exec();
}
if (!File.Exists(toolPath))
{
- Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!"));
+ Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!"));
return false;
}
@@ -32,7 +35,7 @@ namespace SourceGit.Commands
return false;
}
- cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\"";
+ cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {fileArg}";
return cmd.Exec();
}
@@ -51,7 +54,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath))
{
- Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!"));
+ Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!"));
return false;
}
diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs
index 732530f5..698fbfce 100644
--- a/src/Commands/Pull.cs
+++ b/src/Commands/Pull.cs
@@ -1,33 +1,18 @@
-using System;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Pull : Command
{
- public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, bool prune, Action outputHandler)
+ public Pull(string repo, string remote, string branch, bool useRebase)
{
- _outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
- Args = "pull --verbose --progress --tags ";
+ Args = "pull --verbose --progress ";
if (useRebase)
- Args += "--rebase ";
- if (noTags)
- Args += "--no-tags ";
- if (prune)
- Args += "--prune ";
+ Args += "--rebase=true ";
Args += $"{remote} {branch}";
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler;
}
}
diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs
index 69b859ab..8a5fe33c 100644
--- a/src/Commands/Push.cs
+++ b/src/Commands/Push.cs
@@ -1,16 +1,11 @@
-using System;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Push : Command
{
- public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force, Action onProgress)
+ public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force)
{
- _outputHandler = onProgress;
-
WorkingDirectory = repo;
Context = repo;
- TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "push --progress --verbose ";
@@ -26,7 +21,7 @@ namespace SourceGit.Commands
Args += $"{remote} {local}:{remoteBranch}";
}
- public Push(string repo, string remote, string tag, bool isDelete)
+ public Push(string repo, string remote, string refname, bool isDelete)
{
WorkingDirectory = repo;
Context = repo;
@@ -36,14 +31,7 @@ namespace SourceGit.Commands
if (isDelete)
Args += "--delete ";
- Args += $"{remote} refs/tags/{tag}";
+ Args += $"{remote} {refname}";
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private readonly Action _outputHandler = null;
}
}
diff --git a/src/Commands/QueryAssumeUnchangedFiles.cs b/src/Commands/QueryAssumeUnchangedFiles.cs
new file mode 100644
index 00000000..b5c23b0b
--- /dev/null
+++ b/src/Commands/QueryAssumeUnchangedFiles.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands
+{
+ public partial class QueryAssumeUnchangedFiles : Command
+ {
+ [GeneratedRegex(@"^(\w)\s+(.+)$")]
+ private static partial Regex REG_PARSE();
+
+ public QueryAssumeUnchangedFiles(string repo)
+ {
+ WorkingDirectory = repo;
+ Args = "ls-files -v";
+ RaiseError = false;
+ }
+
+ public List Result()
+ {
+ var outs = new List();
+ var rs = ReadToEnd();
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ var match = REG_PARSE().Match(line);
+ if (!match.Success)
+ continue;
+
+ if (match.Groups[1].Value == "h")
+ outs.Add(match.Groups[2].Value);
+ }
+
+ return outs;
+ }
+ }
+}
diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs
index 95f97214..f268d709 100644
--- a/src/Commands/QueryBranches.cs
+++ b/src/Commands/QueryBranches.cs
@@ -14,22 +14,48 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\"";
+ Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\"";
}
- public List Result()
+ public List Result(out int localBranchesCount)
{
+ localBranchesCount = 0;
+
var branches = new List();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return branches;
- var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ var remoteHeads = new Dictionary();
foreach (var line in lines)
{
var b = ParseLine(line);
if (b != null)
+ {
branches.Add(b);
+ if (!b.IsLocal)
+ remoteHeads.Add(b.FullName, b.Head);
+ else
+ localBranchesCount++;
+ }
+ }
+
+ foreach (var b in branches)
+ {
+ if (b.IsLocal && !string.IsNullOrEmpty(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;
@@ -38,7 +64,7 @@ namespace SourceGit.Commands
private Models.Branch ParseLine(string line)
{
var parts = line.Split('\0');
- if (parts.Length != 5)
+ if (parts.Length != 6)
return null;
var branch = new Models.Branch();
@@ -72,13 +98,16 @@ namespace SourceGit.Commands
}
branch.FullName = refName;
- branch.Head = parts[1];
- branch.IsCurrent = parts[2] == "*";
- branch.Upstream = parts[3];
+ branch.CommitterDate = ulong.Parse(parts[1]);
+ branch.Head = parts[2];
+ branch.IsCurrent = parts[3] == "*";
+ branch.Upstream = parts[4];
+ branch.IsUpstreamGone = false;
- if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
- branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();
- else
+ if (!branch.IsLocal ||
+ string.IsNullOrEmpty(branch.Upstream) ||
+ string.IsNullOrEmpty(parts[5]) ||
+ parts[5].Equals("=", StringComparison.Ordinal))
branch.TrackStatus = new Models.BranchTrackStatus();
return branch;
diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs
index bef09abb..4e99ce7a 100644
--- a/src/Commands/QueryCommitChildren.cs
+++ b/src/Commands/QueryCommitChildren.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace SourceGit.Commands
{
@@ -9,22 +10,26 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
Context = repo;
_commit = commit;
- Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
+ Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}";
}
- public IEnumerable Result()
+ public List Result()
{
- Exec();
- return _lines;
- }
+ var rs = ReadToEnd();
+ var outs = new List();
+ if (rs.IsSuccess)
+ {
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ if (line.Contains(_commit))
+ outs.Add(line.Substring(0, 40));
+ }
+ }
- protected override void OnReadline(string line)
- {
- if (line.Contains(_commit))
- _lines.Add(line.Substring(0, 40));
+ return outs;
}
private string _commit;
- private List _lines = new List();
}
}
diff --git a/src/Commands/QueryCommitFullMessage.cs b/src/Commands/QueryCommitFullMessage.cs
index c8f1867d..36b6d1c7 100644
--- a/src/Commands/QueryCommitFullMessage.cs
+++ b/src/Commands/QueryCommitFullMessage.cs
@@ -6,7 +6,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"show --no-show-signature --pretty=format:%B -s {sha}";
+ Args = $"show --no-show-signature --format=%B -s {sha}";
}
public string Result()
diff --git a/src/Commands/QueryCommitSignInfo.cs b/src/Commands/QueryCommitSignInfo.cs
index 5c81cf57..133949af 100644
--- a/src/Commands/QueryCommitSignInfo.cs
+++ b/src/Commands/QueryCommitSignInfo.cs
@@ -7,7 +7,7 @@
WorkingDirectory = repo;
Context = repo;
- const string baseArgs = "show --no-show-signature --pretty=format:\"%G?%n%GS%n%GK\" -s";
+ const string baseArgs = "show --no-show-signature --format=%G?%n%GS%n%GK -s";
const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null";
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
}
@@ -18,7 +18,7 @@
if (!rs.IsSuccess)
return null;
- var raw = rs.StdOut.Trim();
+ var raw = rs.StdOut.Trim().ReplaceLineEndings("\n");
if (raw.Length <= 1)
return null;
@@ -29,7 +29,6 @@
Signer = lines[1],
Key = lines[2]
};
-
}
}
}
diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs
index 6318f331..8ac9cbc5 100644
--- a/src/Commands/QueryCommits.cs
+++ b/src/Commands/QueryCommits.cs
@@ -10,7 +10,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"log --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
+ Args = $"log --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
_findFirstMerged = needFindHead;
}
@@ -18,20 +18,20 @@ namespace SourceGit.Commands
{
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
- if (method == Models.CommitSearchMethod.ByUser)
+ if (method == Models.CommitSearchMethod.ByAuthor)
{
- search += $"-i --author=\"{filter}\" --committer=\"{filter}\"";
+ search += $"-i --author=\"{filter}\"";
}
- else if (method == Models.CommitSearchMethod.ByFile)
+ else if (method == Models.CommitSearchMethod.ByCommitter)
{
- search += $"-- \"{filter}\"";
+ search += $"-i --committer=\"{filter}\"";
}
- else
+ else if (method == Models.CommitSearchMethod.ByMessage)
{
var argsBuilder = new StringBuilder();
argsBuilder.Append(search);
- var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
@@ -41,10 +41,18 @@ namespace SourceGit.Commands
search = argsBuilder.ToString();
}
+ else if (method == Models.CommitSearchMethod.ByPath)
+ {
+ search += $"-- \"{filter}\"";
+ }
+ else
+ {
+ search = $"-G\"{filter}\"";
+ }
WorkingDirectory = repo;
Context = repo;
- Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=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;
}
@@ -120,7 +128,7 @@ namespace SourceGit.Commands
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
- var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0)
return;
diff --git a/src/Commands/QueryCommitsWithFullMessage.cs b/src/Commands/QueryCommitsForInteractiveRebase.cs
similarity index 83%
rename from src/Commands/QueryCommitsWithFullMessage.cs
rename to src/Commands/QueryCommitsForInteractiveRebase.cs
index c15cdbe1..9f238319 100644
--- a/src/Commands/QueryCommitsWithFullMessage.cs
+++ b/src/Commands/QueryCommitsForInteractiveRebase.cs
@@ -3,18 +3,18 @@ using System.Collections.Generic;
namespace SourceGit.Commands
{
- public class QueryCommitsWithFullMessage : Command
+ public class QueryCommitsForInteractiveRebase : Command
{
- public QueryCommitsWithFullMessage(string repo, string args)
+ public QueryCommitsForInteractiveRebase(string repo, string on)
{
_boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----";
WorkingDirectory = repo;
Context = repo;
- Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {args}";
+ Args = $"log --date-order --no-show-signature --decorate=full --format=\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {on}..HEAD";
}
- public List Result()
+ public List Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
@@ -29,7 +29,7 @@ namespace SourceGit.Commands
switch (nextPartIdx)
{
case 0:
- _current = new Models.CommitWithMessage();
+ _current = new Models.InteractiveCommit();
_current.Commit.SHA = line;
_commits.Add(_current);
break;
@@ -52,7 +52,7 @@ namespace SourceGit.Commands
_current.Commit.CommitterTime = ulong.Parse(line);
break;
default:
- var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
+ var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal);
if (boundary > end)
{
_current.Message = rs.StdOut.Substring(start, boundary - start - 1);
@@ -88,8 +88,8 @@ namespace SourceGit.Commands
_current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
- private List _commits = new List();
- private Models.CommitWithMessage _current = null;
- private string _boundary = "";
+ private List _commits = [];
+ private Models.InteractiveCommit _current = null;
+ private readonly string _boundary;
}
}
diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs
index f887859c..648b9058 100644
--- a/src/Commands/QueryFileContent.cs
+++ b/src/Commands/QueryFileContent.cs
@@ -35,5 +35,39 @@ namespace SourceGit.Commands
return stream;
}
+
+ public static Stream FromLFS(string repo, string oid, long size)
+ {
+ var starter = new ProcessStartInfo();
+ starter.WorkingDirectory = repo;
+ starter.FileName = Native.OS.GitExecutable;
+ starter.Arguments = "lfs smudge";
+ starter.UseShellExecute = false;
+ starter.CreateNoWindow = true;
+ starter.WindowStyle = ProcessWindowStyle.Hidden;
+ starter.RedirectStandardInput = true;
+ starter.RedirectStandardOutput = true;
+
+ var stream = new MemoryStream();
+ try
+ {
+ var proc = new Process() { StartInfo = starter };
+ proc.Start();
+ proc.StandardInput.WriteLine("version https://git-lfs.github.com/spec/v1");
+ proc.StandardInput.WriteLine($"oid sha256:{oid}");
+ proc.StandardInput.WriteLine($"size {size}");
+ proc.StandardOutput.BaseStream.CopyTo(stream);
+ proc.WaitForExit();
+ proc.Close();
+
+ stream.Position = 0;
+ }
+ catch (Exception e)
+ {
+ App.RaiseException(repo, $"Failed to query file content: {e}");
+ }
+
+ return stream;
+ }
}
}
diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs
index 9016d826..30af7715 100644
--- a/src/Commands/QueryFileSize.cs
+++ b/src/Commands/QueryFileSize.cs
@@ -16,9 +16,6 @@ namespace SourceGit.Commands
public long Result()
{
- if (_result != 0)
- return _result;
-
var rs = ReadToEnd();
if (rs.IsSuccess)
{
@@ -29,7 +26,5 @@ namespace SourceGit.Commands
return 0;
}
-
- private readonly long _result = 0;
}
}
diff --git a/src/Commands/QueryGitCommonDir.cs b/src/Commands/QueryGitCommonDir.cs
new file mode 100644
index 00000000..1076243e
--- /dev/null
+++ b/src/Commands/QueryGitCommonDir.cs
@@ -0,0 +1,26 @@
+using System.IO;
+
+namespace SourceGit.Commands
+{
+ public class QueryGitCommonDir : Command
+ {
+ public QueryGitCommonDir(string workDir)
+ {
+ WorkingDirectory = workDir;
+ Args = "rev-parse --git-common-dir";
+ RaiseError = false;
+ }
+
+ public string Result()
+ {
+ var rs = ReadToEnd().StdOut;
+ if (string.IsNullOrEmpty(rs))
+ return null;
+
+ rs = rs.Trim();
+ if (Path.IsPathRooted(rs))
+ return rs;
+ return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
+ }
+ }
+}
diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs
index ea422215..788ed617 100644
--- a/src/Commands/QueryLocalChanges.cs
+++ b/src/Commands/QueryLocalChanges.cs
@@ -1,6 +1,9 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Text.RegularExpressions;
+using Avalonia.Threading;
+
namespace SourceGit.Commands
{
public partial class QueryLocalChanges : Command
@@ -13,144 +16,150 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
+ Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List Result()
{
- Exec();
- return _changes;
- }
-
- protected override void OnReadline(string line)
- {
- var match = REG_FORMAT().Match(line);
- if (!match.Success)
- return;
-
- var change = new Models.Change() { Path = match.Groups[2].Value };
- var status = match.Groups[1].Value;
-
- switch (status)
+ var outs = new List();
+ var rs = ReadToEnd();
+ if (!rs.IsSuccess)
{
- case " M":
- change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
- break;
- case " T":
- change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
- break;
- case " A":
- change.Set(Models.ChangeState.None, Models.ChangeState.Added);
- break;
- case " D":
- change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
- break;
- case " R":
- change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
- break;
- case " C":
- change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
- break;
- case "M":
- change.Set(Models.ChangeState.Modified);
- break;
- case "MM":
- change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
- break;
- case "MT":
- change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
- break;
- case "MD":
- change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
- break;
- case "T":
- change.Set(Models.ChangeState.TypeChanged);
- break;
- case "TM":
- change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
- break;
- case "TT":
- change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
- break;
- case "TD":
- change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
- break;
- case "A":
- change.Set(Models.ChangeState.Added);
- break;
- case "AM":
- change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
- break;
- case "AT":
- change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
- break;
- case "AD":
- change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
- break;
- case "D":
- change.Set(Models.ChangeState.Deleted);
- break;
- case "R":
- change.Set(Models.ChangeState.Renamed);
- break;
- case "RM":
- change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
- break;
- case "RT":
- change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
- break;
- case "RD":
- change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
- break;
- case "C":
- change.Set(Models.ChangeState.Copied);
- break;
- case "CM":
- change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
- break;
- case "CT":
- change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
- break;
- case "CD":
- change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
- break;
- case "DR":
- change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed);
- break;
- case "DC":
- change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied);
- break;
- case "DD":
- change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted);
- break;
- case "AU":
- change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged);
- break;
- case "UD":
- change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted);
- break;
- case "UA":
- change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added);
- break;
- case "DU":
- change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged);
- break;
- case "AA":
- change.Set(Models.ChangeState.Added, Models.ChangeState.Added);
- break;
- case "UU":
- change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged);
- break;
- case "??":
- change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked);
- break;
- default:
- return;
+ Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr));
+ return outs;
}
- _changes.Add(change);
- }
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ var match = REG_FORMAT().Match(line);
+ if (!match.Success)
+ continue;
- private readonly List _changes = new List();
+ var change = new Models.Change() { Path = match.Groups[2].Value };
+ var status = match.Groups[1].Value;
+
+ switch (status)
+ {
+ case " M":
+ change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
+ break;
+ case " T":
+ change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
+ break;
+ case " A":
+ change.Set(Models.ChangeState.None, Models.ChangeState.Added);
+ break;
+ case " D":
+ change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
+ break;
+ case " R":
+ change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
+ break;
+ case " C":
+ change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
+ break;
+ case "M":
+ change.Set(Models.ChangeState.Modified);
+ break;
+ case "MM":
+ change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
+ break;
+ case "MT":
+ change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
+ break;
+ case "MD":
+ change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
+ break;
+ case "T":
+ change.Set(Models.ChangeState.TypeChanged);
+ break;
+ case "TM":
+ change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
+ break;
+ case "TT":
+ change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
+ break;
+ case "TD":
+ change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
+ break;
+ case "A":
+ change.Set(Models.ChangeState.Added);
+ break;
+ case "AM":
+ change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
+ break;
+ case "AT":
+ change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
+ break;
+ case "AD":
+ change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
+ break;
+ case "D":
+ change.Set(Models.ChangeState.Deleted);
+ break;
+ case "R":
+ change.Set(Models.ChangeState.Renamed);
+ break;
+ case "RM":
+ change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
+ break;
+ case "RT":
+ change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
+ break;
+ case "RD":
+ change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
+ break;
+ case "C":
+ change.Set(Models.ChangeState.Copied);
+ break;
+ case "CM":
+ change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
+ break;
+ case "CT":
+ change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
+ break;
+ case "CD":
+ change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
+ break;
+ case "DD":
+ change.ConflictReason = Models.ConflictReason.BothDeleted;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "AU":
+ change.ConflictReason = Models.ConflictReason.AddedByUs;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "UD":
+ change.ConflictReason = Models.ConflictReason.DeletedByThem;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "UA":
+ change.ConflictReason = Models.ConflictReason.AddedByThem;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "DU":
+ change.ConflictReason = Models.ConflictReason.DeletedByUs;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "AA":
+ change.ConflictReason = Models.ConflictReason.BothAdded;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "UU":
+ change.ConflictReason = Models.ConflictReason.BothModified;
+ change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
+ break;
+ case "??":
+ change.Set(Models.ChangeState.None, Models.ChangeState.Untracked);
+ break;
+ }
+
+ if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None)
+ outs.Add(change);
+ }
+
+ return outs;
+ }
}
}
diff --git a/src/Commands/QueryRefsContainsCommit.cs b/src/Commands/QueryRefsContainsCommit.cs
index 82e9b341..cabe1b50 100644
--- a/src/Commands/QueryRefsContainsCommit.cs
+++ b/src/Commands/QueryRefsContainsCommit.cs
@@ -20,7 +20,7 @@ namespace SourceGit.Commands
if (!output.IsSuccess)
return rs;
- var lines = output.StdOut.Split('\n');
+ var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.EndsWith("/HEAD", StringComparison.Ordinal))
diff --git a/src/Commands/QueryRemotes.cs b/src/Commands/QueryRemotes.cs
index b5b41b4a..7afec74d 100644
--- a/src/Commands/QueryRemotes.cs
+++ b/src/Commands/QueryRemotes.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
@@ -17,27 +18,31 @@ namespace SourceGit.Commands
public List Result()
{
- Exec();
- return _loaded;
- }
+ var outs = new List();
+ var rs = ReadToEnd();
+ if (!rs.IsSuccess)
+ return outs;
- protected override void OnReadline(string line)
- {
- var match = REG_REMOTE().Match(line);
- if (!match.Success)
- return;
-
- var remote = new Models.Remote()
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
{
- Name = match.Groups[1].Value,
- URL = match.Groups[2].Value,
- };
+ var match = REG_REMOTE().Match(line);
+ if (!match.Success)
+ continue;
- if (_loaded.Find(x => x.Name == remote.Name) != null)
- return;
- _loaded.Add(remote);
+ var remote = new Models.Remote()
+ {
+ Name = match.Groups[1].Value,
+ URL = match.Groups[2].Value,
+ };
+
+ if (outs.Find(x => x.Name == remote.Name) != null)
+ continue;
+
+ outs.Add(remote);
+ }
+
+ return outs;
}
-
- private readonly List _loaded = new List();
}
}
diff --git a/src/Commands/QueryRevisionByRefName.cs b/src/Commands/QueryRevisionByRefName.cs
new file mode 100644
index 00000000..7fb4ecfa
--- /dev/null
+++ b/src/Commands/QueryRevisionByRefName.cs
@@ -0,0 +1,21 @@
+namespace SourceGit.Commands
+{
+ public class QueryRevisionByRefName : Command
+ {
+ public QueryRevisionByRefName(string repo, string refname)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ Args = $"rev-parse {refname}";
+ }
+
+ public string Result()
+ {
+ var rs = ReadToEnd();
+ if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut))
+ return rs.StdOut.Trim();
+
+ return null;
+ }
+ }
+}
diff --git a/src/Commands/QueryRevisionFileNames.cs b/src/Commands/QueryRevisionFileNames.cs
index d2d69614..c6fd7373 100644
--- a/src/Commands/QueryRevisionFileNames.cs
+++ b/src/Commands/QueryRevisionFileNames.cs
@@ -1,4 +1,6 @@
-namespace SourceGit.Commands
+using System.Collections.Generic;
+
+namespace SourceGit.Commands
{
public class QueryRevisionFileNames : Command
{
@@ -9,13 +11,17 @@
Args = $"ls-tree -r -z --name-only {revision}";
}
- public string[] Result()
+ public List Result()
{
var rs = ReadToEnd();
- if (rs.IsSuccess)
- return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
+ if (!rs.IsSuccess)
+ return [];
- return [];
+ var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
+ var outs = new List();
+ foreach (var line in lines)
+ outs.Add(line);
+ return outs;
}
}
}
diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs
index de3406e8..5c582dcc 100644
--- a/src/Commands/QueryRevisionObjects.cs
+++ b/src/Commands/QueryRevisionObjects.cs
@@ -51,21 +51,14 @@ namespace SourceGit.Commands
obj.Type = Models.ObjectType.Blob;
obj.Path = match.Groups[3].Value;
- switch (match.Groups[1].Value)
+ obj.Type = match.Groups[1].Value switch
{
- case "blob":
- obj.Type = Models.ObjectType.Blob;
- break;
- case "tree":
- obj.Type = Models.ObjectType.Tree;
- break;
- case "tag":
- obj.Type = Models.ObjectType.Tag;
- break;
- case "commit":
- obj.Type = Models.ObjectType.Commit;
- break;
- }
+ "blob" => Models.ObjectType.Blob,
+ "tree" => Models.ObjectType.Tree,
+ "tag" => Models.ObjectType.Tag,
+ "commit" => Models.ObjectType.Commit,
+ _ => obj.Type,
+ };
_objects.Add(obj);
}
diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs
index 1e1c9ea4..35289ec5 100644
--- a/src/Commands/QuerySingleCommit.cs
+++ b/src/Commands/QuerySingleCommit.cs
@@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
+ Args = $"show --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
}
public Models.Commit Result()
diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs
index f5c9659f..78980401 100644
--- a/src/Commands/QueryStagedChangesWithAmend.cs
+++ b/src/Commands/QueryStagedChangesWithAmend.cs
@@ -6,87 +6,87 @@ namespace SourceGit.Commands
{
public partial class QueryStagedChangesWithAmend : Command
{
- [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")]
+ [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")]
private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")]
private static partial Regex REG_FORMAT2();
- public QueryStagedChangesWithAmend(string repo)
+ public QueryStagedChangesWithAmend(string repo, string parent)
{
WorkingDirectory = repo;
Context = repo;
- Args = "diff-index --cached -M HEAD^";
+ Args = $"diff-index --cached -M {parent}";
+ _parent = parent;
}
public List Result()
{
var rs = ReadToEnd();
- if (rs.IsSuccess)
+ if (!rs.IsSuccess)
+ return [];
+
+ var changes = new List();
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
{
- var changes = new List();
- var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
- foreach (var line in lines)
+ var match = REG_FORMAT2().Match(line);
+ if (match.Success)
{
- var match = REG_FORMAT2().Match(line);
- if (match.Success)
+ var change = new Models.Change()
{
- var change = new Models.Change()
+ Path = match.Groups[3].Value,
+ DataForAmend = new Models.ChangeDataForAmend()
{
- Path = match.Groups[3].Value,
- DataForAmend = new Models.ChangeDataForAmend()
- {
- FileMode = match.Groups[1].Value,
- ObjectHash = match.Groups[2].Value,
- },
- };
- change.Set(Models.ChangeState.Renamed);
- changes.Add(change);
- continue;
- }
-
- match = REG_FORMAT1().Match(line);
- if (match.Success)
- {
- var change = new Models.Change()
- {
- Path = match.Groups[4].Value,
- DataForAmend = new Models.ChangeDataForAmend()
- {
- FileMode = match.Groups[1].Value,
- ObjectHash = match.Groups[2].Value,
- },
- };
-
- var type = match.Groups[3].Value;
- switch (type)
- {
- case "A":
- change.Set(Models.ChangeState.Added);
- break;
- case "C":
- change.Set(Models.ChangeState.Copied);
- break;
- case "D":
- change.Set(Models.ChangeState.Deleted);
- break;
- case "M":
- change.Set(Models.ChangeState.Modified);
- break;
- case "T":
- change.Set(Models.ChangeState.TypeChanged);
- break;
- case "U":
- change.Set(Models.ChangeState.Unmerged);
- break;
- }
- changes.Add(change);
- }
+ FileMode = match.Groups[1].Value,
+ ObjectHash = match.Groups[2].Value,
+ ParentSHA = _parent,
+ },
+ };
+ change.Set(Models.ChangeState.Renamed);
+ changes.Add(change);
+ continue;
}
- return changes;
+ match = REG_FORMAT1().Match(line);
+ if (match.Success)
+ {
+ var change = new Models.Change()
+ {
+ Path = match.Groups[4].Value,
+ DataForAmend = new Models.ChangeDataForAmend()
+ {
+ FileMode = match.Groups[1].Value,
+ ObjectHash = match.Groups[2].Value,
+ ParentSHA = _parent,
+ },
+ };
+
+ var type = match.Groups[3].Value;
+ switch (type)
+ {
+ case "A":
+ change.Set(Models.ChangeState.Added);
+ break;
+ case "C":
+ change.Set(Models.ChangeState.Copied);
+ break;
+ case "D":
+ change.Set(Models.ChangeState.Deleted);
+ break;
+ case "M":
+ change.Set(Models.ChangeState.Modified);
+ break;
+ case "T":
+ change.Set(Models.ChangeState.TypeChanged);
+ break;
+ }
+ changes.Add(change);
+ }
}
- return [];
+ return changes;
}
+
+ private readonly string _parent;
}
}
diff --git a/src/Commands/QueryStashes.cs b/src/Commands/QueryStashes.cs
index ccf601c5..2a84b34a 100644
--- a/src/Commands/QueryStashes.cs
+++ b/src/Commands/QueryStashes.cs
@@ -9,52 +9,60 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = "stash list --pretty=format:%H%n%P%n%ct%n%gd%n%s";
+ Args = "stash list -z --no-show-signature --format=\"%H%n%P%n%ct%n%gd%n%B\"";
}
public List Result()
{
- Exec();
- return _stashes;
- }
+ var outs = new List();
+ var rs = ReadToEnd();
+ if (!rs.IsSuccess)
+ return outs;
- protected override void OnReadline(string line)
- {
- switch (_nextLineIdx)
+ var items = rs.StdOut.Split('\0', StringSplitOptions.RemoveEmptyEntries);
+ foreach (var item in items)
{
- case 0:
- _current = new Models.Stash() { SHA = line };
- _stashes.Add(_current);
- break;
- case 1:
- ParseParent(line);
- break;
- case 2:
- _current.Time = ulong.Parse(line);
- break;
- case 3:
- _current.Name = line;
- break;
- case 4:
- _current.Message = line;
- break;
+ var current = new Models.Stash();
+
+ var nextPartIdx = 0;
+ var start = 0;
+ var end = item.IndexOf('\n', start);
+ while (end > 0 && nextPartIdx < 4)
+ {
+ var line = item.Substring(start, end - start);
+
+ switch (nextPartIdx)
+ {
+ case 0:
+ current.SHA = line;
+ break;
+ case 1:
+ if (line.Length > 6)
+ current.Parents.AddRange(line.Split(' ', StringSplitOptions.RemoveEmptyEntries));
+ break;
+ case 2:
+ current.Time = ulong.Parse(line);
+ break;
+ case 3:
+ current.Name = line;
+ break;
+ }
+
+ nextPartIdx++;
+
+ start = end + 1;
+ if (start >= item.Length - 1)
+ break;
+
+ end = item.IndexOf('\n', start);
+ }
+
+ if (start < item.Length)
+ current.Message = item.Substring(start);
+
+ outs.Add(current);
}
-
- _nextLineIdx++;
- if (_nextLineIdx > 4)
- _nextLineIdx = 0;
+ return outs;
}
-
- private void ParseParent(string data)
- {
- if (data.Length < 8)
- return;
-
- _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
- }
-
- private readonly List _stashes = new List();
- private Models.Stash _current = null;
- private int _nextLineIdx = 0;
}
}
diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs
index 5fd6e3d5..663c0ea0 100644
--- a/src/Commands/QuerySubmodules.cs
+++ b/src/Commands/QuerySubmodules.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
@@ -6,12 +7,12 @@ namespace SourceGit.Commands
{
public partial class QuerySubmodules : Command
{
- [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
- private static partial Regex REG_FORMAT1();
- [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
- private static partial Regex REG_FORMAT2();
- [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")]
+ [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")]
private static partial Regex REG_FORMAT_STATUS();
+ [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")]
+ private static partial Regex REG_FORMAT_DIRTY();
+ [GeneratedRegex(@"^submodule\.(\S*)\.(\w+)=(.*)$")]
+ private static partial Regex REG_FORMAT_MODULE_INFO();
public QuerySubmodules(string repo)
{
@@ -24,55 +25,118 @@ namespace SourceGit.Commands
{
var submodules = new List();
var rs = ReadToEnd();
- if (!rs.IsSuccess)
- return submodules;
- var builder = new StringBuilder();
- var lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ var map = new Dictionary();
+ var needCheckLocalChanges = false;
foreach (var line in lines)
{
- var match = REG_FORMAT1().Match(line);
+ var match = REG_FORMAT_STATUS().Match(line);
if (match.Success)
{
- var path = match.Groups[1].Value;
- builder.Append($"\"{path}\" ");
- submodules.Add(new Models.Submodule() { Path = path });
- continue;
- }
+ var stat = match.Groups[1].Value;
+ var sha = match.Groups[2].Value;
+ var path = match.Groups[3].Value;
- match = REG_FORMAT2().Match(line);
- if (match.Success)
- {
- var path = match.Groups[1].Value;
- builder.Append($"\"{path}\" ");
- submodules.Add(new Models.Submodule() { Path = path });
+ var module = new Models.Submodule() { Path = path, SHA = sha };
+ switch (stat[0])
+ {
+ case '-':
+ module.Status = Models.SubmoduleStatus.NotInited;
+ break;
+ case '+':
+ module.Status = Models.SubmoduleStatus.RevisionChanged;
+ break;
+ case 'U':
+ module.Status = Models.SubmoduleStatus.Unmerged;
+ break;
+ default:
+ module.Status = Models.SubmoduleStatus.Normal;
+ needCheckLocalChanges = true;
+ break;
+ }
+
+ map.Add(path, module);
+ submodules.Add(module);
}
}
if (submodules.Count > 0)
{
- Args = $"status -uno --porcelain -- {builder}";
+ Args = "config --file .gitmodules --list";
+ rs = ReadToEnd();
+ if (rs.IsSuccess)
+ {
+ var modules = new Dictionary();
+ lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ var match = REG_FORMAT_MODULE_INFO().Match(line);
+ if (match.Success)
+ {
+ var name = match.Groups[1].Value;
+ var key = match.Groups[2].Value;
+ var val = match.Groups[3].Value;
+
+ if (!modules.TryGetValue(name, out var m))
+ {
+ m = new ModuleInfo();
+ modules.Add(name, m);
+ }
+
+ if (key.Equals("path", StringComparison.Ordinal))
+ m.Path = val;
+ else if (key.Equals("url", StringComparison.Ordinal))
+ m.URL = val;
+ }
+ }
+
+ foreach (var kv in modules)
+ {
+ if (map.TryGetValue(kv.Value.Path, out var m))
+ m.URL = kv.Value.URL;
+ }
+ }
+ }
+
+ if (needCheckLocalChanges)
+ {
+ var builder = new StringBuilder();
+ foreach (var kv in map)
+ {
+ if (kv.Value.Status == Models.SubmoduleStatus.Normal)
+ {
+ builder.Append('"');
+ builder.Append(kv.Key);
+ builder.Append("\" ");
+ }
+ }
+
+ Args = $"--no-optional-locks status --porcelain -- {builder}";
rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;
- var dirty = new HashSet();
- lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
+ lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
- var match = REG_FORMAT_STATUS().Match(line);
+ var match = REG_FORMAT_DIRTY().Match(line);
if (match.Success)
{
var path = match.Groups[1].Value;
- dirty.Add(path);
+ if (map.TryGetValue(path, out var m))
+ m.Status = Models.SubmoduleStatus.Modified;
}
}
-
- foreach (var submodule in submodules)
- submodule.IsDirty = dirty.Contains(submodule.Path);
}
return submodules;
}
+
+ private class ModuleInfo
+ {
+ public string Path { get; set; } = string.Empty;
+ public string URL { get; set; } = string.Empty;
+ }
}
}
diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs
index 73f63d8b..896d555e 100644
--- a/src/Commands/QueryTags.cs
+++ b/src/Commands/QueryTags.cs
@@ -11,7 +11,7 @@ namespace SourceGit.Commands
Context = repo;
WorkingDirectory = repo;
- Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\"";
+ Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\"";
}
public List Result()
@@ -24,17 +24,22 @@ namespace SourceGit.Commands
var records = rs.StdOut.Split(_boundary, StringSplitOptions.RemoveEmptyEntries);
foreach (var record in records)
{
- var subs = record.Split('\0', StringSplitOptions.None);
- if (subs.Length != 5)
+ var subs = record.Split('\0');
+ if (subs.Length != 6)
continue;
- var message = subs[4].Trim();
+ var name = subs[0].Substring(10);
+ var message = subs[5].Trim();
+ if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal))
+ message = null;
+
tags.Add(new Models.Tag()
{
- Name = subs[0].Substring(10),
- SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
- CreatorDate = ulong.Parse(subs[3]),
- Message = string.IsNullOrEmpty(message) ? null : message,
+ Name = name,
+ IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal),
+ SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3],
+ CreatorDate = ulong.Parse(subs[4]),
+ Message = message,
});
}
diff --git a/src/Commands/QueryTrackStatus.cs b/src/Commands/QueryTrackStatus.cs
index 0bf9746f..e7e1f1c9 100644
--- a/src/Commands/QueryTrackStatus.cs
+++ b/src/Commands/QueryTrackStatus.cs
@@ -19,7 +19,7 @@ namespace SourceGit.Commands
if (!rs.IsSuccess)
return status;
- var lines = rs.StdOut.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line[0] == '>')
diff --git a/src/Commands/QueryUpdatableSubmodules.cs b/src/Commands/QueryUpdatableSubmodules.cs
new file mode 100644
index 00000000..03f4a24d
--- /dev/null
+++ b/src/Commands/QueryUpdatableSubmodules.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands
+{
+ public partial class QueryUpdatableSubmodules : Command
+ {
+ [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")]
+ private static partial Regex REG_FORMAT_STATUS();
+
+ public QueryUpdatableSubmodules(string repo)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ Args = "submodule status";
+ }
+
+ public List Result()
+ {
+ var submodules = new List();
+ var rs = ReadToEnd();
+
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ var match = REG_FORMAT_STATUS().Match(line);
+ if (match.Success)
+ {
+ var stat = match.Groups[1].Value;
+ var path = match.Groups[3].Value;
+ if (!stat.StartsWith(' '))
+ submodules.Add(path);
+ }
+ }
+
+ return submodules;
+ }
+ }
+}
diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs
index da272135..6a54533b 100644
--- a/src/Commands/Reset.cs
+++ b/src/Commands/Reset.cs
@@ -1,33 +1,7 @@
-using System.Collections.Generic;
-using System.Text;
-
-namespace SourceGit.Commands
+namespace SourceGit.Commands
{
public class Reset : Command
{
- public Reset(string repo)
- {
- WorkingDirectory = repo;
- Context = repo;
- Args = "reset";
- }
-
- public Reset(string repo, List changes)
- {
- WorkingDirectory = repo;
- Context = repo;
-
- var builder = new StringBuilder();
- builder.Append("reset --");
- foreach (var c in changes)
- {
- builder.Append(" \"");
- builder.Append(c.Path);
- builder.Append("\"");
- }
- Args = builder.ToString();
- }
-
public Reset(string repo, string revision, string mode)
{
WorkingDirectory = repo;
diff --git a/src/Commands/Restore.cs b/src/Commands/Restore.cs
index 7a363543..663ea975 100644
--- a/src/Commands/Restore.cs
+++ b/src/Commands/Restore.cs
@@ -1,29 +1,52 @@
-using System.Collections.Generic;
-using System.Text;
+using System.Text;
namespace SourceGit.Commands
{
public class Restore : Command
{
- public Restore(string repo)
+ ///
+ /// Only used for single staged change.
+ ///
+ ///
+ ///
+ public Restore(string repo, Models.Change stagedChange)
{
WorkingDirectory = repo;
Context = repo;
- Args = "restore . --source=HEAD --staged --worktree --recurse-submodules";
+
+ var builder = new StringBuilder();
+ builder.Append("restore --staged -- \"");
+ builder.Append(stagedChange.Path);
+ builder.Append('"');
+
+ if (stagedChange.Index == Models.ChangeState.Renamed)
+ {
+ builder.Append(" \"");
+ builder.Append(stagedChange.OriginalPath);
+ builder.Append('"');
+ }
+
+ Args = builder.ToString();
}
- public Restore(string repo, List files, string extra)
+ ///
+ /// Restore changes given in a path-spec file.
+ ///
+ ///
+ ///
+ ///
+ public Restore(string repo, string pathspecFile, bool isStaged)
{
WorkingDirectory = repo;
Context = repo;
- StringBuilder builder = new StringBuilder();
+ var builder = new StringBuilder();
builder.Append("restore ");
- if (!string.IsNullOrEmpty(extra))
- builder.Append(extra).Append(" ");
- builder.Append("--");
- foreach (var f in files)
- builder.Append(' ').Append('"').Append(f).Append('"');
+ builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules ");
+ builder.Append("--pathspec-from-file=\"");
+ builder.Append(pathspecFile);
+ builder.Append('"');
+
Args = builder.ToString();
}
}
diff --git a/src/Commands/SaveChangesAsPatch.cs b/src/Commands/SaveChangesAsPatch.cs
index 461bbfb5..b10037a1 100644
--- a/src/Commands/SaveChangesAsPatch.cs
+++ b/src/Commands/SaveChangesAsPatch.cs
@@ -37,6 +37,19 @@ namespace SourceGit.Commands
return true;
}
+ public static bool ProcessStashChanges(string repo, List opts, string saveTo)
+ {
+ using (var sw = File.Create(saveTo))
+ {
+ foreach (var opt in opts)
+ {
+ if (!ProcessSingleChange(repo, opt, sw))
+ return false;
+ }
+ }
+ return true;
+ }
+
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
{
var starter = new ProcessStartInfo();
diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs
index 99e89093..b6127ea6 100644
--- a/src/Commands/SaveRevisionFile.cs
+++ b/src/Commands/SaveRevisionFile.cs
@@ -10,15 +10,15 @@ namespace SourceGit.Commands
{
public static void Run(string repo, string revision, string file, string saveTo)
{
+ var dir = Path.GetDirectoryName(saveTo);
+ if (!Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result();
if (isLFSFiltered)
{
- var tmpFile = saveTo + ".tmp";
- if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile))
- {
- ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
- }
- File.Delete(tmpFile);
+ var pointerStream = QueryFileContent.Run(repo, revision, file);
+ ExecCmd(repo, "lfs smudge", saveTo, pointerStream);
}
else
{
@@ -26,7 +26,7 @@ namespace SourceGit.Commands
}
}
- private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
+ private static void ExecCmd(string repo, string args, string outputFile, Stream input = null)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
@@ -45,27 +45,11 @@ namespace SourceGit.Commands
{
var proc = new Process() { StartInfo = starter };
proc.Start();
-
- if (inputFile != null)
- {
- using (StreamReader sr = new StreamReader(inputFile))
- {
- while (true)
- {
- var line = sr.ReadLine();
- if (line == null)
- break;
- proc.StandardInput.WriteLine(line);
- }
- }
- }
-
+ if (input != null)
+ proc.StandardInput.Write(new StreamReader(input).ReadToEnd());
proc.StandardOutput.BaseStream.CopyTo(sw);
proc.WaitForExit();
- var rs = proc.ExitCode == 0;
proc.Close();
-
- return rs;
}
catch (Exception e)
{
@@ -73,7 +57,6 @@ namespace SourceGit.Commands
{
App.RaiseException(repo, "Save file failed: " + e.Message);
});
- return false;
}
}
}
diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs
index 1cbf4b2a..7d1a269b 100644
--- a/src/Commands/Stash.cs
+++ b/src/Commands/Stash.cs
@@ -30,7 +30,7 @@ namespace SourceGit.Commands
public bool Push(string message, List changes, bool keepIndex)
{
var builder = new StringBuilder();
- builder.Append("stash push ");
+ builder.Append("stash push --include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
@@ -47,7 +47,7 @@ namespace SourceGit.Commands
public bool Push(string message, string pathspecFromFile, bool keepIndex)
{
var builder = new StringBuilder();
- builder.Append("stash push --pathspec-from-file=\"");
+ builder.Append("stash push --include-untracked --pathspec-from-file=\"");
builder.Append(pathspecFromFile);
builder.Append("\" ");
if (keepIndex)
@@ -73,21 +73,22 @@ namespace SourceGit.Commands
return Exec();
}
- public bool Apply(string name)
+ public bool Apply(string name, bool restoreIndex)
{
- Args = $"stash apply --index -q {name}";
+ var opts = restoreIndex ? "--index" : string.Empty;
+ Args = $"stash apply -q {opts} \"{name}\"";
return Exec();
}
public bool Pop(string name)
{
- Args = $"stash pop --index -q {name}";
+ Args = $"stash pop -q --index \"{name}\"";
return Exec();
}
public bool Drop(string name)
{
- Args = $"stash drop -q {name}";
+ Args = $"stash drop -q \"{name}\"";
return Exec();
}
diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs
index 511c43e8..e11c1740 100644
--- a/src/Commands/Statistics.cs
+++ b/src/Commands/Statistics.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace SourceGit.Commands
{
@@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
+ Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE";
}
public Models.Statistics Result()
@@ -40,7 +40,7 @@ namespace SourceGit.Commands
if (dateEndIdx == -1)
return;
- var dateStr = line.Substring(0, dateEndIdx);
+ var dateStr = line.AsSpan(0, dateEndIdx);
if (double.TryParse(dateStr, out var date))
statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
}
diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs
index 9a273703..025d035a 100644
--- a/src/Commands/Submodule.cs
+++ b/src/Commands/Submodule.cs
@@ -1,4 +1,5 @@
-using System;
+using System.Collections.Generic;
+using System.Text;
namespace SourceGit.Commands
{
@@ -10,10 +11,9 @@ namespace SourceGit.Commands
Context = repo;
}
- public bool Add(string url, string relativePath, bool recursive, Action outputHandler)
+ public bool Add(string url, string relativePath, bool recursive)
{
- _outputHandler = outputHandler;
- Args = $"submodule add {url} \"{relativePath}\"";
+ Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\"";
if (!Exec())
return false;
@@ -29,38 +29,38 @@ namespace SourceGit.Commands
}
}
- public bool Update(string module, bool init, bool recursive, bool useRemote, Action outputHandler)
+ public bool Update(List modules, bool init, bool recursive, bool useRemote = false)
{
- Args = "submodule update";
+ var builder = new StringBuilder();
+ builder.Append("submodule update");
if (init)
- Args += " --init";
+ builder.Append(" --init");
if (recursive)
- Args += " --recursive";
+ builder.Append(" --recursive");
if (useRemote)
- Args += " --remote";
- if (!string.IsNullOrEmpty(module))
- Args += $" -- \"{module}\"";
+ builder.Append(" --remote");
+ if (modules.Count > 0)
+ {
+ builder.Append(" --");
+ foreach (var module in modules)
+ builder.Append($" \"{module}\"");
+ }
- _outputHandler = outputHandler;
+ Args = builder.ToString();
return Exec();
}
- public bool Delete(string relativePath)
+ public bool Deinit(string module, bool force)
{
- Args = $"submodule deinit -f \"{relativePath}\"";
- if (!Exec())
- return false;
-
- Args = $"rm -rf \"{relativePath}\"";
+ Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\"";
return Exec();
}
- protected override void OnReadline(string line)
+ public bool Delete(string module)
{
- _outputHandler?.Invoke(line);
+ Args = $"rm -rf \"{module}\"";
+ return Exec();
}
-
- private Action _outputHandler;
}
}
diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs
index fa11e366..017afea0 100644
--- a/src/Commands/Tag.cs
+++ b/src/Commands/Tag.cs
@@ -1,59 +1,51 @@
-using System.Collections.Generic;
-using System.IO;
+using System.IO;
namespace SourceGit.Commands
{
public static class Tag
{
- public static bool Add(string repo, string name, string basedOn)
+ public static bool Add(string repo, string name, string basedOn, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
- cmd.Args = $"tag {name} {basedOn}";
+ cmd.Args = $"tag --no-sign {name} {basedOn}";
+ cmd.Log = log;
return cmd.Exec();
}
- public static bool Add(string repo, string name, string basedOn, string message, bool sign)
+ public static bool Add(string repo, string name, string basedOn, string message, bool sign, Models.ICommandLog log)
{
var param = sign ? "--sign -a" : "--no-sign -a";
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag {param} {name} {basedOn} ";
+ cmd.Log = log;
if (!string.IsNullOrEmpty(message))
{
string tmp = Path.GetTempFileName();
File.WriteAllText(tmp, message);
cmd.Args += $"-F \"{tmp}\"";
- }
- else
- {
- cmd.Args += $"-m {name}";
+
+ var succ = cmd.Exec();
+ File.Delete(tmp);
+ return succ;
}
+ cmd.Args += $"-m {name}";
return cmd.Exec();
}
- public static bool Delete(string repo, string name, List remotes)
+ public static bool Delete(string repo, string name, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag --delete {name}";
- if (!cmd.Exec())
- return false;
-
- if (remotes != null)
- {
- foreach (var r in remotes)
- {
- new Push(repo, r.Name, name, true).Exec();
- }
- }
-
- return true;
+ cmd.Log = log;
+ return cmd.Exec();
}
}
}
diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UnstageChangesForAmend.cs
index c930f136..19def067 100644
--- a/src/Commands/UnstageChangesForAmend.cs
+++ b/src/Commands/UnstageChangesForAmend.cs
@@ -23,13 +23,11 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t");
_patchBuilder.Append(c.OriginalPath);
- _patchBuilder.Append("\n");
}
else if (c.Index == Models.ChangeState.Added)
{
_patchBuilder.Append("0 0000000000000000000000000000000000000000\t");
_patchBuilder.Append(c.Path);
- _patchBuilder.Append("\n");
}
else if (c.Index == Models.ChangeState.Deleted)
{
@@ -37,7 +35,6 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t");
_patchBuilder.Append(c.Path);
- _patchBuilder.Append("\n");
}
else
{
@@ -46,8 +43,9 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t");
_patchBuilder.Append(c.Path);
- _patchBuilder.Append("\n");
}
+
+ _patchBuilder.Append("\n");
}
}
diff --git a/src/Commands/UpdateRef.cs b/src/Commands/UpdateRef.cs
deleted file mode 100644
index ba1b3d2f..00000000
--- a/src/Commands/UpdateRef.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-
-namespace SourceGit.Commands
-{
- public class UpdateRef : Command
- {
- public UpdateRef(string repo, string refName, string toRevision, Action outputHandler)
- {
- _outputHandler = outputHandler;
-
- WorkingDirectory = repo;
- Context = repo;
- Args = $"update-ref {refName} {toRevision}";
- }
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private Action _outputHandler;
- }
-}
diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs
index 7516b1e3..1198a443 100644
--- a/src/Commands/Worktree.cs
+++ b/src/Commands/Worktree.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
namespace SourceGit.Commands
{
@@ -20,12 +21,13 @@ namespace SourceGit.Commands
var last = null as Models.Worktree;
if (rs.IsSuccess)
{
- var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.StartsWith("worktree ", StringComparison.Ordinal))
{
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
+ last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath);
worktrees.Add(last);
}
else if (line.StartsWith("bare", StringComparison.Ordinal))
@@ -54,7 +56,7 @@ namespace SourceGit.Commands
return worktrees;
}
- public bool Add(string fullpath, string name, bool createNew, string tracking, Action outputHandler)
+ public bool Add(string fullpath, string name, bool createNew, string tracking)
{
Args = "worktree add ";
@@ -73,15 +75,15 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(tracking))
Args += tracking;
+ else if (!string.IsNullOrEmpty(name) && !createNew)
+ Args += name;
- _outputHandler = outputHandler;
return Exec();
}
- public bool Prune(Action outputHandler)
+ public bool Prune()
{
Args = "worktree prune -v";
- _outputHandler = outputHandler;
return Exec();
}
@@ -97,22 +99,14 @@ namespace SourceGit.Commands
return Exec();
}
- public bool Remove(string fullpath, bool force, Action outputHandler)
+ public bool Remove(string fullpath, bool force)
{
if (force)
Args = $"worktree remove -f \"{fullpath}\"";
else
Args = $"worktree remove \"{fullpath}\"";
- _outputHandler = outputHandler;
return Exec();
}
-
- protected override void OnReadline(string line)
- {
- _outputHandler?.Invoke(line);
- }
-
- private Action _outputHandler = null;
}
}
diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs
index 2d738700..3563fb37 100644
--- a/src/Converters/BoolConverters.cs
+++ b/src/Converters/BoolConverters.cs
@@ -1,4 +1,5 @@
using Avalonia.Data.Converters;
+using Avalonia.Media;
namespace SourceGit.Converters
{
@@ -6,5 +7,8 @@ namespace SourceGit.Converters
{
public static readonly FuncValueConverter ToPageTabWidth =
new FuncValueConverter(x => x ? 200 : double.NaN);
+
+ public static readonly FuncValueConverter IsBoldToFontWeight =
+ new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Normal);
}
}
diff --git a/src/Converters/FilterModeConverters.cs b/src/Converters/FilterModeConverters.cs
index c486af5e..016613e8 100644
--- a/src/Converters/FilterModeConverters.cs
+++ b/src/Converters/FilterModeConverters.cs
@@ -8,15 +8,12 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter ToBorderBrush =
new FuncValueConverter(v =>
{
- switch (v)
+ return v switch
{
- case Models.FilterMode.Included:
- return Brushes.Green;
- case Models.FilterMode.Excluded:
- return Brushes.Red;
- default:
- return Brushes.Transparent;
- }
+ Models.FilterMode.Included => Brushes.Green,
+ Models.FilterMode.Excluded => Brushes.Red,
+ _ => Brushes.Transparent,
+ };
});
}
}
diff --git a/src/Converters/InteractiveRebaseActionConverters.cs b/src/Converters/InteractiveRebaseActionConverters.cs
index dbd183bd..3534c809 100644
--- a/src/Converters/InteractiveRebaseActionConverters.cs
+++ b/src/Converters/InteractiveRebaseActionConverters.cs
@@ -8,42 +8,19 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter ToIconBrush =
new FuncValueConverter(v =>
{
- switch (v)
+ return v switch
{
- case Models.InteractiveRebaseAction.Pick:
- return Brushes.Green;
- case Models.InteractiveRebaseAction.Edit:
- return Brushes.Orange;
- case Models.InteractiveRebaseAction.Reword:
- return Brushes.Orange;
- case Models.InteractiveRebaseAction.Squash:
- return Brushes.LightGray;
- case Models.InteractiveRebaseAction.Fixup:
- return Brushes.LightGray;
- default:
- return Brushes.Red;
- }
+ Models.InteractiveRebaseAction.Pick => Brushes.Green,
+ Models.InteractiveRebaseAction.Edit => Brushes.Orange,
+ Models.InteractiveRebaseAction.Reword => Brushes.Orange,
+ Models.InteractiveRebaseAction.Squash => Brushes.LightGray,
+ Models.InteractiveRebaseAction.Fixup => Brushes.LightGray,
+ _ => Brushes.Red,
+ };
});
public static readonly FuncValueConverter ToName =
- new FuncValueConverter(v =>
- {
- switch (v)
- {
- case Models.InteractiveRebaseAction.Pick:
- return "Pick";
- case Models.InteractiveRebaseAction.Edit:
- return "Edit";
- case Models.InteractiveRebaseAction.Reword:
- return "Reword";
- case Models.InteractiveRebaseAction.Squash:
- return "Squash";
- case Models.InteractiveRebaseAction.Fixup:
- return "Fixup";
- default:
- return "Drop";
- }
- });
+ new FuncValueConverter(v => v.ToString());
public static readonly FuncValueConverter CanEditMessage =
new FuncValueConverter(v => v == Models.InteractiveRebaseAction.Reword || v == Models.InteractiveRebaseAction.Squash);
diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs
index 81cac8b7..6f3ae98b 100644
--- a/src/Converters/ListConverters.cs
+++ b/src/Converters/ListConverters.cs
@@ -7,8 +7,11 @@ namespace SourceGit.Converters
{
public static class ListConverters
{
+ public static readonly FuncValueConverter Count =
+ new FuncValueConverter(v => v == null ? "0" : $"{v.Count}");
+
public static readonly FuncValueConverter ToCount =
- new FuncValueConverter(v => v == null ? " (0)" : $" ({v.Count})");
+ new FuncValueConverter(v => v == null ? "(0)" : $"({v.Count})");
public static readonly FuncValueConverter IsNullOrEmpty =
new FuncValueConverter(v => v == null || v.Count == 0);
diff --git a/src/Converters/ObjectConverters.cs b/src/Converters/ObjectConverters.cs
new file mode 100644
index 00000000..f7c57764
--- /dev/null
+++ b/src/Converters/ObjectConverters.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace SourceGit.Converters
+{
+ public static class ObjectConverters
+ {
+ public class IsTypeOfConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || parameter == null)
+ return false;
+
+ return value.GetType().IsAssignableTo((Type)parameter);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return new NotImplementedException();
+ }
+ }
+
+ public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter();
+ }
+}
diff --git a/src/Converters/PathConverters.cs b/src/Converters/PathConverters.cs
index dd7cfa49..ac1e61e5 100644
--- a/src/Converters/PathConverters.cs
+++ b/src/Converters/PathConverters.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using Avalonia.Data.Converters;
@@ -22,7 +22,7 @@ namespace SourceGit.Converters
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length;
if (v.StartsWith(home, StringComparison.Ordinal))
- return "~" + v.Substring(prefixLen);
+ return $"~{v.AsSpan(prefixLen)}";
return v;
});
diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs
index e6f4237c..bcadfae9 100644
--- a/src/Converters/StringConverters.cs
+++ b/src/Converters/StringConverters.cs
@@ -78,5 +78,11 @@ namespace SourceGit.Converters
return v.Substring(13);
return v;
});
+
+ public static readonly FuncValueConverter ContainsSpaces =
+ new FuncValueConverter(v => v != null && v.Contains(' '));
+
+ public static readonly FuncValueConverter IsNotNullOrWhitespace =
+ new FuncValueConverter(v => v != null && v.Trim().Length > 0);
}
}
diff --git a/src/Models/ApplyWhiteSpaceMode.cs b/src/Models/ApplyWhiteSpaceMode.cs
index 6fbce0b2..aad45f57 100644
--- a/src/Models/ApplyWhiteSpaceMode.cs
+++ b/src/Models/ApplyWhiteSpaceMode.cs
@@ -2,14 +2,22 @@
{
public class ApplyWhiteSpaceMode
{
+ public static readonly ApplyWhiteSpaceMode[] Supported =
+ [
+ new ApplyWhiteSpaceMode("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
+ new ApplyWhiteSpaceMode("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
+ new ApplyWhiteSpaceMode("Error", "Raise errors and refuses to apply the patch", "error"),
+ new ApplyWhiteSpaceMode("Error All", "Similar to 'error', but shows more", "error-all"),
+ ];
+
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public ApplyWhiteSpaceMode(string n, string d, string a)
{
- Name = App.Text(n);
- Desc = App.Text(d);
+ Name = n;
+ Desc = d;
Arg = a;
}
}
diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs
index 9f0bceaf..f0006652 100644
--- a/src/Models/AvatarManager.cs
+++ b/src/Models/AvatarManager.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -17,7 +17,7 @@ namespace SourceGit.Models
{
public interface IAvatarHost
{
- void OnAvatarResourceChanged(string email);
+ void OnAvatarResourceChanged(string email, Bitmap image);
}
public partial class AvatarManager
@@ -26,10 +26,7 @@ namespace SourceGit.Models
{
get
{
- if (_instance == null)
- _instance = new AvatarManager();
-
- return _instance;
+ return _instance ??= new AvatarManager();
}
}
@@ -38,7 +35,7 @@ namespace SourceGit.Models
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")]
private static partial Regex REG_GITHUB_USER_EMAIL();
- private object _synclock = new object();
+ private readonly Lock _synclock = new();
private string _storePath;
private List _avatars = new List();
private Dictionary _resources = new Dictionary();
@@ -119,7 +116,7 @@ namespace SourceGit.Models
Dispatcher.UIThread.InvokeAsync(() =>
{
_resources[email] = img;
- NotifyResourceChanged(email);
+ NotifyResourceChanged(email, img);
});
}
@@ -144,14 +141,13 @@ namespace SourceGit.Models
if (_defaultAvatars.Contains(email))
return null;
- if (_resources.ContainsKey(email))
- _resources.Remove(email);
+ _resources.Remove(email);
var localFile = Path.Combine(_storePath, GetEmailHash(email));
if (File.Exists(localFile))
File.Delete(localFile);
- NotifyResourceChanged(email);
+ NotifyResourceChanged(email, null);
}
else
{
@@ -179,13 +175,40 @@ namespace SourceGit.Models
lock (_synclock)
{
- if (!_requesting.Contains(email))
- _requesting.Add(email);
+ _requesting.Add(email);
}
return null;
}
+ public void SetFromLocal(string email, string file)
+ {
+ try
+ {
+ Bitmap image;
+
+ using (var stream = File.OpenRead(file))
+ {
+ image = Bitmap.DecodeToWidth(stream, 128);
+ }
+
+ if (image == null)
+ return;
+
+ _resources[email] = image;
+
+ _requesting.Remove(email);
+
+ var store = Path.Combine(_storePath, GetEmailHash(email));
+ File.Copy(file, store, true);
+ NotifyResourceChanged(email, image);
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+
private void LoadDefaultAvatar(string key, string img)
{
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute));
@@ -196,19 +219,17 @@ namespace SourceGit.Models
private string GetEmailHash(string email)
{
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
- var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
- var builder = new StringBuilder();
+ var hash = MD5.HashData(Encoding.Default.GetBytes(lowered));
+ var builder = new StringBuilder(hash.Length * 2);
foreach (var c in hash)
builder.Append(c.ToString("x2"));
return builder.ToString();
}
- private void NotifyResourceChanged(string email)
+ private void NotifyResourceChanged(string email, Bitmap image)
{
foreach (var avatar in _avatars)
- {
- avatar.OnAvatarResourceChanged(email);
- }
+ avatar.OnAvatarResourceChanged(email, image);
}
}
}
diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs
new file mode 100644
index 00000000..2ed8beb2
--- /dev/null
+++ b/src/Models/Bisect.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+
+namespace SourceGit.Models
+{
+ public enum BisectState
+ {
+ None = 0,
+ WaitingForRange,
+ Detecting,
+ }
+
+ [Flags]
+ public enum BisectCommitFlag
+ {
+ None = 0,
+ Good = 1 << 0,
+ Bad = 1 << 1,
+ }
+
+ public class Bisect
+ {
+ public HashSet Bads
+ {
+ get;
+ set;
+ } = [];
+
+ public HashSet Goods
+ {
+ get;
+ set;
+ } = [];
+ }
+}
diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs
index 0ba320c1..7146da3f 100644
--- a/src/Models/Branch.cs
+++ b/src/Models/Branch.cs
@@ -23,10 +23,17 @@ namespace SourceGit.Models
}
}
+ public enum BranchSortMode
+ {
+ Name = 0,
+ CommitterDate,
+ }
+
public class Branch
{
public string Name { get; set; }
public string FullName { get; set; }
+ public ulong CommitterDate { get; set; }
public string Head { get; set; }
public bool IsLocal { get; set; }
public bool IsCurrent { get; set; }
@@ -34,6 +41,7 @@ namespace SourceGit.Models
public string Upstream { get; set; }
public BranchTrackStatus TrackStatus { get; set; }
public string Remote { get; set; }
+ public bool IsUpstreamGone { get; set; }
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
}
diff --git a/src/Models/Change.cs b/src/Models/Change.cs
index 36fe20ac..129678be 100644
--- a/src/Models/Change.cs
+++ b/src/Models/Change.cs
@@ -18,14 +18,27 @@ namespace SourceGit.Models
Deleted,
Renamed,
Copied,
- Unmerged,
- Untracked
+ Untracked,
+ Conflicted,
+ }
+
+ public enum ConflictReason
+ {
+ None,
+ BothDeleted,
+ AddedByUs,
+ DeletedByThem,
+ AddedByThem,
+ DeletedByUs,
+ BothAdded,
+ BothModified,
}
public class ChangeDataForAmend
{
public string FileMode { get; set; } = "";
public string ObjectHash { get; set; } = "";
+ public string ParentSHA { get; set; } = "";
}
public class Change
@@ -35,20 +48,14 @@ namespace SourceGit.Models
public string Path { get; set; } = "";
public string OriginalPath { get; set; } = "";
public ChangeDataForAmend DataForAmend { get; set; } = null;
+ public ConflictReason ConflictReason { get; set; } = ConflictReason.None;
- public bool IsConflit
- {
- get
- {
- if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged)
- return true;
- if (Index == ChangeState.Added && WorkTree == ChangeState.Added)
- return true;
- if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted)
- return true;
- return false;
- }
- }
+ public bool IsConflicted => WorkTree == ChangeState.Conflicted;
+ public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason];
+ public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason];
+
+ public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree];
+ public string IndexDesc => TYPE_DESCS[(int)Index];
public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{
@@ -76,8 +83,44 @@ namespace SourceGit.Models
if (Path[0] == '"')
Path = Path.Substring(1, Path.Length - 2);
+
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"')
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
}
+
+ private static readonly string[] TYPE_DESCS =
+ [
+ "Unknown",
+ "Modified",
+ "Type Changed",
+ "Added",
+ "Deleted",
+ "Renamed",
+ "Copied",
+ "Untracked",
+ "Conflict"
+ ];
+ private static readonly string[] CONFLICT_MARKERS =
+ [
+ string.Empty,
+ "DD",
+ "AU",
+ "UD",
+ "UA",
+ "DU",
+ "AA",
+ "UU"
+ ];
+ private static readonly string[] CONFLICT_DESCS =
+ [
+ string.Empty,
+ "Both deleted",
+ "Added by us",
+ "Deleted by them",
+ "Added by them",
+ "Deleted by us",
+ "Both added",
+ "Both modified"
+ ];
}
}
diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs
index 9bc7f0c3..1bfed55b 100644
--- a/src/Models/Commit.cs
+++ b/src/Models/Commit.cs
@@ -8,13 +8,19 @@ namespace SourceGit.Models
{
public enum CommitSearchMethod
{
- ByUser,
+ BySHA = 0,
+ ByAuthor,
+ ByCommitter,
ByMessage,
- ByFile,
+ ByPath,
+ ByContent,
}
public class Commit
{
+ // As retrieved by: git mktree Parents { get; set; } = new List();
- public List Decorators { get; set; } = new List();
+ public List Parents { get; set; } = new();
+ public List Decorators { get; set; } = new();
public bool HasDecorators => Decorators.Count > 0;
- public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
- public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
- public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
+ public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime);
+ public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime);
+ public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly);
+ public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly);
public bool IsMerged { get; set; } = false;
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
@@ -42,7 +49,7 @@ namespace SourceGit.Models
public int Color { get; set; } = 0;
public double Opacity => IsMerged ? 1 : OpacityForNotMerged;
public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular;
- public Thickness Margin { get; set; } = new Thickness(0);
+ public Thickness Margin { get; set; } = new(0);
public IBrush Brush => CommitGraph.Pens[Color].Brush;
public void ParseDecorators(string data)
@@ -106,14 +113,14 @@ namespace SourceGit.Models
if (l.Type != r.Type)
return (int)l.Type - (int)r.Type;
else
- return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
+ return NumericSort.Compare(l.Name, r.Name);
});
}
}
- public class CommitWithMessage
+ public class CommitFullMessage
{
- public Commit Commit { get; set; } = new Commit();
- public string Message { get; set; } = "";
+ public string Message { get; set; } = string.Empty;
+ public InlineElementCollector Inlines { get; set; } = new();
}
}
diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs
index 77209751..01488656 100644
--- a/src/Models/CommitGraph.cs
+++ b/src/Models/CommitGraph.cs
@@ -64,8 +64,8 @@ namespace SourceGit.Models
{
const double unitWidth = 12;
const double halfWidth = 6;
- const double unitHeight = 28;
- const double halfHeight = 14;
+ const double unitHeight = 1;
+ const double halfHeight = 0.5;
var temp = new CommitGraph();
var unsolved = new List();
diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs
index 955779a8..2891e5d6 100644
--- a/src/Models/CommitLink.cs
+++ b/src/Models/CommitLink.cs
@@ -1,8 +1,49 @@
-namespace SourceGit.Models
+using System;
+using System.Collections.Generic;
+
+namespace SourceGit.Models
{
public class CommitLink
{
public string Name { get; set; } = null;
public string URLPrefix { get; set; } = null;
+
+ public CommitLink(string name, string prefix)
+ {
+ Name = name;
+ URLPrefix = prefix;
+ }
+
+ public static List Get(List remotes)
+ {
+ var outs = new List();
+
+ foreach (var remote in remotes)
+ {
+ if (remote.TryGetVisitURL(out var url))
+ {
+ var trimmedUrl = url.AsSpan();
+ if (url.EndsWith(".git"))
+ trimmedUrl = url.AsSpan(0, url.Length - 4);
+
+ if (url.StartsWith("https://github.com/", StringComparison.Ordinal))
+ outs.Add(new($"Github ({trimmedUrl.Slice(19)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal))
+ outs.Add(new($"GitLab ({trimmedUrl.Slice(trimmedUrl.Slice(15).IndexOf('/') + 16)})", $"{url}/-/commit/"));
+ else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal))
+ outs.Add(new($"Gitee ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal))
+ outs.Add(new($"BitBucket ({trimmedUrl.Slice(22)})", $"{url}/commits/"));
+ else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal))
+ outs.Add(new($"Codeberg ({trimmedUrl.Slice(21)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal))
+ outs.Add(new($"Gitea ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal))
+ outs.Add(new($"sourcehut ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
+ }
+ }
+
+ return outs;
+ }
}
}
diff --git a/src/Models/CommitSignInfo.cs b/src/Models/CommitSignInfo.cs
index 44b95e61..99317e94 100644
--- a/src/Models/CommitSignInfo.cs
+++ b/src/Models/CommitSignInfo.cs
@@ -13,21 +13,13 @@ namespace SourceGit.Models
{
get
{
- switch (VerifyResult)
+ return VerifyResult switch
{
- case 'G':
- case 'U':
- return Brushes.Green;
- case 'X':
- case 'Y':
- case 'R':
- return Brushes.DarkOrange;
- case 'B':
- case 'E':
- return Brushes.Red;
- default:
- return Brushes.Transparent;
- }
+ 'G' or 'U' => Brushes.Green,
+ 'X' or 'Y' or 'R' => Brushes.DarkOrange,
+ 'B' or 'E' => Brushes.Red,
+ _ => Brushes.Transparent,
+ };
}
}
@@ -35,25 +27,17 @@ namespace SourceGit.Models
{
get
{
- switch (VerifyResult)
+ return VerifyResult switch
{
- case 'G':
- return "Good signature.";
- case 'U':
- return "Good signature with unknown validity.";
- case 'X':
- return "Good signature but has expired.";
- case 'Y':
- return "Good signature made by expired key.";
- case 'R':
- return "Good signature made by a revoked key.";
- case 'B':
- return "Bad signature.";
- case 'E':
- return "Signature cannot be checked.";
- default:
- return "No signature.";
- }
+ 'G' => "Good signature.",
+ 'U' => "Good signature with unknown validity.",
+ 'X' => "Good signature but has expired.",
+ 'Y' => "Good signature made by expired key.",
+ 'R' => "Good signature made by a revoked key.",
+ 'B' => "Bad signature.",
+ 'E' => "Signature cannot be checked.",
+ _ => "No signature.",
+ };
}
}
}
diff --git a/src/Models/CommitTemplate.cs b/src/Models/CommitTemplate.cs
index 56e1992c..3f331543 100644
--- a/src/Models/CommitTemplate.cs
+++ b/src/Models/CommitTemplate.cs
@@ -4,7 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models
{
- public partial class CommitTemplate : ObservableObject
+ public class CommitTemplate : ObservableObject
{
public string Name
{
diff --git a/src/Models/ConventionalCommitType.cs b/src/Models/ConventionalCommitType.cs
index 4fb61d87..531a16c0 100644
--- a/src/Models/ConventionalCommitType.cs
+++ b/src/Models/ConventionalCommitType.cs
@@ -4,23 +4,28 @@ namespace SourceGit.Models
{
public class ConventionalCommitType
{
- public string Type { get; set; } = string.Empty;
- public string Description { get; set; } = string.Empty;
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public string Description { get; set; }
- public static readonly List Supported = new List()
- {
- new ConventionalCommitType("feat", "Adding a new feature"),
- new ConventionalCommitType("fix", "Fixing a bug"),
- new ConventionalCommitType("docs", "Updating documentation"),
- new ConventionalCommitType("style", "Elements or code styles without changing the code logic"),
- new ConventionalCommitType("test", "Adding or updating tests"),
- new ConventionalCommitType("chore", "Making changes to the build process or auxiliary tools and libraries"),
- new ConventionalCommitType("revert", "Undoing a previous commit"),
- new ConventionalCommitType("refactor", "Restructuring code without changing its external behavior")
- };
+ public static readonly List Supported = [
+ new("Features", "feat", "Adding a new feature"),
+ new("Bug Fixes", "fix", "Fixing a bug"),
+ new("Work In Progress", "wip", "Still being developed and not yet complete"),
+ new("Reverts", "revert", "Undoing a previous commit"),
+ new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"),
+ new("Performance Improvements", "perf", "Improves performance"),
+ new("Builds", "build", "Changes that affect the build system or external dependencies"),
+ new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"),
+ new("Documentations", "docs", "Updating documentation"),
+ new("Styles", "style", "Elements or code styles without changing the code logic"),
+ new("Tests", "test", "Adding or updating tests"),
+ new("Chores", "chore", "Other changes that don't modify src or test files"),
+ ];
- public ConventionalCommitType(string type, string description)
+ public ConventionalCommitType(string name, string type, string description)
{
+ Name = name;
Type = type;
Description = description;
}
diff --git a/src/Models/Count.cs b/src/Models/Count.cs
new file mode 100644
index 00000000..d48b0c08
--- /dev/null
+++ b/src/Models/Count.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace SourceGit.Models
+{
+ public class Count : IDisposable
+ {
+ public int Value { get; set; } = 0;
+
+ public Count(int value)
+ {
+ Value = value;
+ }
+
+ public void Dispose()
+ {
+ // Ignore
+ }
+ }
+}
diff --git a/src/Models/CustomAction.cs b/src/Models/CustomAction.cs
index 8452a42d..c6130fe0 100644
--- a/src/Models/CustomAction.cs
+++ b/src/Models/CustomAction.cs
@@ -1,4 +1,5 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using Avalonia.Collections;
+using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models
{
@@ -6,6 +7,54 @@ namespace SourceGit.Models
{
Repository,
Commit,
+ Branch,
+ Tag,
+ }
+
+ public enum CustomActionControlType
+ {
+ TextBox = 0,
+ PathSelector,
+ CheckBox,
+ }
+
+ public class CustomActionControl : ObservableObject
+ {
+ public CustomActionControlType Type
+ {
+ get => _type;
+ set => SetProperty(ref _type, value);
+ }
+
+ public string Label
+ {
+ get => _label;
+ set => SetProperty(ref _label, value);
+ }
+
+ public string Description
+ {
+ get => _description;
+ set => SetProperty(ref _description, value);
+ }
+
+ public string StringValue
+ {
+ get => _stringValue;
+ set => SetProperty(ref _stringValue, value);
+ }
+
+ public bool BoolValue
+ {
+ get => _boolValue;
+ set => SetProperty(ref _boolValue, value);
+ }
+
+ private CustomActionControlType _type = CustomActionControlType.TextBox;
+ private string _label = string.Empty;
+ private string _description = string.Empty;
+ private string _stringValue = string.Empty;
+ private bool _boolValue = false;
}
public class CustomAction : ObservableObject
@@ -34,9 +83,22 @@ namespace SourceGit.Models
set => SetProperty(ref _arguments, value);
}
+ public AvaloniaList Controls
+ {
+ get;
+ set;
+ } = [];
+
+ public bool WaitForExit
+ {
+ get => _waitForExit;
+ set => SetProperty(ref _waitForExit, value);
+ }
+
private string _name = string.Empty;
private CustomActionScope _scope = CustomActionScope.Repository;
private string _executable = string.Empty;
private string _arguments = string.Empty;
+ private bool _waitForExit = true;
}
}
diff --git a/src/Models/DateTimeFormat.cs b/src/Models/DateTimeFormat.cs
index 4e71a74f..16276c40 100644
--- a/src/Models/DateTimeFormat.cs
+++ b/src/Models/DateTimeFormat.cs
@@ -25,24 +25,24 @@ namespace SourceGit.Models
set;
} = 0;
- public static DateTimeFormat Actived
+ public static DateTimeFormat Active
{
get => Supported[ActiveIndex];
}
public static readonly List Supported = new List
{
- new DateTimeFormat("yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss"),
- new DateTimeFormat("yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss"),
- new DateTimeFormat("yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss"),
- new DateTimeFormat("MM/dd/yyyy", "MM/dd/yyyy HH:mm:ss"),
- new DateTimeFormat("MM.dd.yyyy", "MM.dd.yyyy HH:mm:ss"),
- new DateTimeFormat("MM-dd-yyyy", "MM-dd-yyyy HH:mm:ss"),
- new DateTimeFormat("dd/MM/yyyy", "dd/MM/yyyy HH:mm:ss"),
- new DateTimeFormat("dd.MM.yyyy", "dd.MM.yyyy HH:mm:ss"),
- new DateTimeFormat("dd-MM-yyyy", "dd-MM-yyyy HH:mm:ss"),
- new DateTimeFormat("MMM d yyyy", "MMM d yyyy HH:mm:ss"),
- new DateTimeFormat("d MMM yyyy", "d MMM yyyy HH:mm:ss"),
+ new DateTimeFormat("yyyy/MM/dd", "yyyy/MM/dd, HH:mm:ss"),
+ new DateTimeFormat("yyyy.MM.dd", "yyyy.MM.dd, HH:mm:ss"),
+ new DateTimeFormat("yyyy-MM-dd", "yyyy-MM-dd, HH:mm:ss"),
+ new DateTimeFormat("MM/dd/yyyy", "MM/dd/yyyy, HH:mm:ss"),
+ new DateTimeFormat("MM.dd.yyyy", "MM.dd.yyyy, HH:mm:ss"),
+ new DateTimeFormat("MM-dd-yyyy", "MM-dd-yyyy, HH:mm:ss"),
+ new DateTimeFormat("dd/MM/yyyy", "dd/MM/yyyy, HH:mm:ss"),
+ new DateTimeFormat("dd.MM.yyyy", "dd.MM.yyyy, HH:mm:ss"),
+ new DateTimeFormat("dd-MM-yyyy", "dd-MM-yyyy, HH:mm:ss"),
+ new DateTimeFormat("MMM d yyyy", "MMM d yyyy, HH:mm:ss"),
+ new DateTimeFormat("d MMM yyyy", "d MMM yyyy, HH:mm:ss"),
};
private static readonly DateTime _example = new DateTime(2025, 1, 31, 8, 0, 0, DateTimeKind.Local);
diff --git a/src/Models/DealWithChangesAfterStashing.cs b/src/Models/DealWithChangesAfterStashing.cs
new file mode 100644
index 00000000..63889c96
--- /dev/null
+++ b/src/Models/DealWithChangesAfterStashing.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace SourceGit.Models
+{
+ public class DealWithChangesAfterStashing
+ {
+ public string Label { get; set; }
+ public string Desc { get; set; }
+
+ public static readonly List Supported = [
+ new ("Discard", "All (or selected) changes will be discarded"),
+ new ("Keep Index", "Staged changes are left intact"),
+ new ("Keep All", "All (or selected) changes are left intact"),
+ ];
+
+ public DealWithChangesAfterStashing(string label, string desc)
+ {
+ Label = label;
+ Desc = desc;
+ }
+ }
+}
diff --git a/src/Models/DealWithLocalChanges.cs b/src/Models/DealWithLocalChanges.cs
deleted file mode 100644
index f308a90c..00000000
--- a/src/Models/DealWithLocalChanges.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace SourceGit.Models
-{
- public enum DealWithLocalChanges
- {
- DoNothing,
- StashAndReaply,
- Discard,
- }
-}
diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs
index 98387e7f..69f93980 100644
--- a/src/Models/DiffOption.cs
+++ b/src/Models/DiffOption.cs
@@ -5,6 +5,15 @@ namespace SourceGit.Models
{
public class DiffOption
{
+ ///
+ /// Enable `--ignore-cr-at-eol` by default?
+ ///
+ public static bool IgnoreCRAtEOL
+ {
+ get;
+ set;
+ } = true;
+
public Change WorkingCopyChange => _workingCopyChange;
public bool IsUnstaged => _isUnstaged;
public List Revisions => _revisions;
@@ -40,7 +49,7 @@ namespace SourceGit.Models
else
{
if (change.DataForAmend != null)
- _extra = "--cached HEAD^";
+ _extra = $"--cached {change.DataForAmend.ParentSHA}";
else
_extra = "--cached";
@@ -56,7 +65,7 @@ namespace SourceGit.Models
///
public DiffOption(Commit commit, Change change)
{
- var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
+ var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^";
_revisions.Add(baseRevision);
_revisions.Add(commit.SHA);
_path = change.Path;
@@ -70,7 +79,7 @@ namespace SourceGit.Models
///
public DiffOption(Commit commit, string file)
{
- var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
+ var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^";
_revisions.Add(baseRevision);
_revisions.Add(commit.SHA);
_path = file;
@@ -115,6 +124,6 @@ namespace SourceGit.Models
private readonly string _path;
private readonly string _orgPath = string.Empty;
private readonly string _extra = string.Empty;
- private readonly List _revisions = new List();
+ private readonly List _revisions = [];
}
}
diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs
index e0ae82e0..2f06c0cf 100644
--- a/src/Models/DiffResult.cs
+++ b/src/Models/DiffResult.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
@@ -16,11 +16,10 @@ namespace SourceGit.Models
Deleted,
}
- public class TextInlineRange
+ public class TextInlineRange(int p, int n)
{
- public int Start { get; set; }
- public int End { get; set; }
- public TextInlineRange(int p, int n) { Start = p; End = p + n - 1; }
+ public int Start { get; set; } = p;
+ public int End { get; set; } = p + n - 1;
}
public class TextDiffLine
@@ -30,6 +29,7 @@ namespace SourceGit.Models
public int OldLineNumber { get; set; } = 0;
public int NewLineNumber { get; set; } = 0;
public List Highlights { get; set; } = new List();
+ public bool NoNewLineEndOfFile { get; set; } = false;
public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString();
public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString();
@@ -100,7 +100,7 @@ namespace SourceGit.Models
rs.HasChanges = true;
break;
}
- else if (isOldSide)
+ if (isOldSide)
{
rs.HasLeftChanges = true;
}
@@ -116,7 +116,7 @@ namespace SourceGit.Models
rs.HasChanges = true;
break;
}
- else if (isOldSide)
+ if (isOldSide)
{
rs.HasChanges = true;
}
@@ -146,7 +146,7 @@ namespace SourceGit.Models
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
{
var isTracked = !string.IsNullOrEmpty(fileBlobGuid);
- var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000";
+ var fileGuid = isTracked ? fileBlobGuid : "00000000";
var builder = new StringBuilder();
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
@@ -163,7 +163,7 @@ namespace SourceGit.Models
if (revert)
{
var totalLines = Lines.Count - 1;
- builder.Append($"@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@");
+ builder.Append("@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@");
for (int i = 1; i <= totalLines; i++)
{
var line = Lines[i];
@@ -555,7 +555,7 @@ namespace SourceGit.Models
}
else if (test.Type == TextDiffLineType.Added)
{
- if (i < start - 1)
+ if (i < start - 1 || isOldSide)
{
if (revert)
{
@@ -565,18 +565,7 @@ namespace SourceGit.Models
}
else
{
- if (isOldSide)
- {
- if (revert)
- {
- newCount++;
- oldCount++;
- }
- }
- else
- {
- newCount++;
- }
+ newCount++;
}
if (i == end - 1 && tailed)
@@ -654,15 +643,7 @@ namespace SourceGit.Models
public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0";
}
- public class NoOrEOLChange
- {
- }
-
- public class FileModeDiff
- {
- public string Old { get; set; } = string.Empty;
- public string New { get; set; } = string.Empty;
- }
+ public class NoOrEOLChange;
public class SubmoduleDiff
{
@@ -681,6 +662,18 @@ namespace SourceGit.Models
public TextDiff TextDiff { get; set; } = null;
public LFSDiff LFSDiff { get; set; } = null;
- public string FileModeChange => string.IsNullOrEmpty(OldMode) ? string.Empty : $"{OldMode} → {NewMode}";
+ public string FileModeChange
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(OldMode) && string.IsNullOrEmpty(NewMode))
+ return string.Empty;
+
+ var oldDisplay = string.IsNullOrEmpty(OldMode) ? "0" : OldMode;
+ var newDisplay = string.IsNullOrEmpty(NewMode) ? "0" : NewMode;
+
+ return $"{oldDisplay} → {newDisplay}";
+ }
+ }
}
}
diff --git a/src/Models/DirtyState.cs b/src/Models/DirtyState.cs
new file mode 100644
index 00000000..2b9d898d
--- /dev/null
+++ b/src/Models/DirtyState.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace SourceGit.Models
+{
+ [Flags]
+ public enum DirtyState
+ {
+ None = 0,
+ HasLocalChanges = 1 << 0,
+ HasPendingPullOrPush = 1 << 1,
+ }
+}
diff --git a/src/Models/ExternalMerger.cs b/src/Models/ExternalMerger.cs
index 49d31df5..fe67ad6a 100644
--- a/src/Models/ExternalMerger.cs
+++ b/src/Models/ExternalMerger.cs
@@ -42,6 +42,8 @@ namespace SourceGit.Models
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e -sw \"$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(10, "plastic_merge", "Plastic SCM", "mergetool.exe", "-s=\"$REMOTE\" -b=\"$BASE\" -d=\"$LOCAL\" -r=\"$MERGED\" --automatic", "-s=\"$LOCAL\" -d=\"$REMOTE\""),
+ new ExternalMerger(11, "meld", "Meld", "Meld.exe", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
};
}
else if (OperatingSystem.IsMacOS())
diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs
index 103e91bc..697c171a 100644
--- a/src/Models/ExternalTool.cs
+++ b/src/Models/ExternalTool.cs
@@ -107,8 +107,7 @@ namespace SourceGit.Models
// Ignore
}
- if (_customPaths == null)
- _customPaths = new ExternalToolPaths();
+ _customPaths ??= new ExternalToolPaths();
}
public void TryAdd(string name, string icon, Func finder, Func execArgsGenerator = null)
diff --git a/src/Models/GitFlow.cs b/src/Models/GitFlow.cs
new file mode 100644
index 00000000..05ade0ac
--- /dev/null
+++ b/src/Models/GitFlow.cs
@@ -0,0 +1,42 @@
+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)
+ {
+ return type switch
+ {
+ GitFlowBranchType.Feature => FeaturePrefix,
+ GitFlowBranchType.Release => ReleasePrefix,
+ GitFlowBranchType.Hotfix => HotfixPrefix,
+ _ => string.Empty,
+ };
+ }
+ }
+}
diff --git a/src/Models/GitIgnoreFile.cs b/src/Models/GitIgnoreFile.cs
new file mode 100644
index 00000000..a23456b6
--- /dev/null
+++ b/src/Models/GitIgnoreFile.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.IO;
+using Avalonia.Media;
+
+namespace SourceGit.Models
+{
+ public class GitIgnoreFile
+ {
+ public static readonly List Supported = [new(true), new(false)];
+
+ public bool IsShared { get; set; }
+ public string File => IsShared ? ".gitignore" : "/info/exclude";
+ public string Desc => IsShared ? "Shared" : "Private";
+ public IBrush Brush => IsShared ? Brushes.Green : Brushes.Gray;
+
+ public GitIgnoreFile(bool isShared)
+ {
+ IsShared = isShared;
+ }
+
+ public string GetFullPath(string repoPath, string gitDir)
+ {
+ return IsShared ? Path.Combine(repoPath, ".gitignore") : Path.Combine(gitDir, "info", "exclude");
+ }
+ }
+}
diff --git a/src/Models/GitVersions.cs b/src/Models/GitVersions.cs
index 394a9518..8aae63a3 100644
--- a/src/Models/GitVersions.cs
+++ b/src/Models/GitVersions.cs
@@ -5,21 +5,16 @@
///
/// The minimal version of Git that required by this app.
///
- public static readonly System.Version MINIMAL = new System.Version(2, 23, 0);
+ public static readonly System.Version MINIMAL = new(2, 25, 1);
///
- /// The minimal version of Git that supports the `add` command with the `--pathspec-from-file` option.
+ /// The minimal version of Git that supports the `stash push` command with the `--pathspec-from-file` option.
///
- public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0);
+ public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new(2, 26, 0);
///
- /// The minimal version of Git that supports the `stash` command with the `--pathspec-from-file` option.
+ /// The minimal version of Git that supports the `stash push` command with the `--staged` option.
///
- public static readonly System.Version STASH_WITH_PATHSPECFILE = new System.Version(2, 26, 0);
-
- ///
- /// The minimal version of Git that supports the `stash` command with the `--staged` option.
- ///
- public static readonly System.Version STASH_ONLY_STAGED = new System.Version(2, 35, 0);
+ public static readonly System.Version STASH_PUSH_ONLY_STAGED = new(2, 35, 0);
}
}
diff --git a/src/Models/Hyperlink.cs b/src/Models/Hyperlink.cs
deleted file mode 100644
index 81dc980e..00000000
--- a/src/Models/Hyperlink.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-namespace SourceGit.Models
-{
- public class Hyperlink
- {
- public int Start { get; set; } = 0;
- public int Length { get; set; } = 0;
- public string Link { get; set; } = "";
- public bool IsCommitSHA { get; set; } = false;
-
- public Hyperlink(int start, int length, string link, bool isCommitSHA = false)
- {
- Start = start;
- Length = length;
- Link = link;
- IsCommitSHA = isCommitSHA;
- }
-
- public bool Intersect(int start, int length)
- {
- if (start == Start)
- return true;
-
- if (start < Start)
- return start + length > Start;
-
- return start < Start + Length;
- }
- }
-}
diff --git a/src/Models/ICommandLog.cs b/src/Models/ICommandLog.cs
new file mode 100644
index 00000000..34ec7031
--- /dev/null
+++ b/src/Models/ICommandLog.cs
@@ -0,0 +1,7 @@
+namespace SourceGit.Models
+{
+ public interface ICommandLog
+ {
+ void AppendLine(string line);
+ }
+}
diff --git a/src/Models/IRepository.cs b/src/Models/IRepository.cs
index 12b1adba..2fc7c612 100644
--- a/src/Models/IRepository.cs
+++ b/src/Models/IRepository.cs
@@ -2,8 +2,7 @@
{
public interface IRepository
{
- string FullPath { get; set; }
- string GitDir { get; set; }
+ bool MayHaveSubmodules();
void RefreshBranches();
void RefreshWorktrees();
diff --git a/src/Models/ImageDecoder.cs b/src/Models/ImageDecoder.cs
new file mode 100644
index 00000000..6fe0f428
--- /dev/null
+++ b/src/Models/ImageDecoder.cs
@@ -0,0 +1,10 @@
+namespace SourceGit.Models
+{
+ public enum ImageDecoder
+ {
+ None = 0,
+ Builtin,
+ Pfim,
+ Tiff,
+ }
+}
diff --git a/src/Models/InlineElement.cs b/src/Models/InlineElement.cs
new file mode 100644
index 00000000..ea7bcee8
--- /dev/null
+++ b/src/Models/InlineElement.cs
@@ -0,0 +1,37 @@
+namespace SourceGit.Models
+{
+ public enum InlineElementType
+ {
+ Keyword = 0,
+ Link,
+ CommitSHA,
+ Code,
+ }
+
+ public class InlineElement
+ {
+ public InlineElementType Type { get; }
+ public int Start { get; }
+ public int Length { get; }
+ public string Link { get; }
+
+ public InlineElement(InlineElementType type, int start, int length, string link)
+ {
+ Type = type;
+ Start = start;
+ Length = length;
+ Link = link;
+ }
+
+ public bool IsIntersecting(int start, int length)
+ {
+ if (start == Start)
+ return true;
+
+ if (start < Start)
+ return start + length > Start;
+
+ return start < Start + Length;
+ }
+ }
+}
diff --git a/src/Models/InlineElementCollector.cs b/src/Models/InlineElementCollector.cs
new file mode 100644
index 00000000..d81aaf8d
--- /dev/null
+++ b/src/Models/InlineElementCollector.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace SourceGit.Models
+{
+ public class InlineElementCollector
+ {
+ public int Count => _implementation.Count;
+ public InlineElement this[int index] => _implementation[index];
+
+ public InlineElement Intersect(int start, int length)
+ {
+ foreach (var elem in _implementation)
+ {
+ if (elem.IsIntersecting(start, length))
+ return elem;
+ }
+
+ return null;
+ }
+
+ public void Add(InlineElement element)
+ {
+ _implementation.Add(element);
+ }
+
+ public void Sort()
+ {
+ _implementation.Sort((l, r) => l.Start.CompareTo(r.Start));
+ }
+
+ public void Clear()
+ {
+ _implementation.Clear();
+ }
+
+ private readonly List _implementation = [];
+ }
+}
diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs
index 0980587a..d1710d4a 100644
--- a/src/Models/InteractiveRebase.cs
+++ b/src/Models/InteractiveRebase.cs
@@ -12,6 +12,12 @@ namespace SourceGit.Models
Drop,
}
+ public class InteractiveCommit
+ {
+ public Commit Commit { get; set; } = new Commit();
+ public string Message { get; set; } = string.Empty;
+ }
+
public class InteractiveRebaseJob
{
public string SHA { get; set; } = string.Empty;
@@ -21,6 +27,8 @@ namespace SourceGit.Models
public class InteractiveRebaseJobCollection
{
+ public string OrigHead { get; set; } = string.Empty;
+ public string Onto { get; set; } = string.Empty;
public List Jobs { get; set; } = new List();
}
}
diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs
new file mode 100644
index 00000000..001c65a6
--- /dev/null
+++ b/src/Models/IpcChannel.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.IO.Pipes;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SourceGit.Models
+{
+ public class IpcChannel : IDisposable
+ {
+ public bool IsFirstInstance { get; }
+
+ public event Action MessageReceived;
+
+ public IpcChannel()
+ {
+ try
+ {
+ _singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ IsFirstInstance = true;
+ _server = new NamedPipeServerStream(
+ "SourceGitIPCChannel" + Environment.UserName,
+ PipeDirection.In,
+ -1,
+ PipeTransmissionMode.Byte,
+ PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
+ _cancellationTokenSource = new CancellationTokenSource();
+ Task.Run(StartServer);
+ }
+ catch
+ {
+ IsFirstInstance = false;
+ }
+ }
+
+ public void SendToFirstInstance(string cmd)
+ {
+ try
+ {
+ using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly))
+ {
+ client.Connect(1000);
+ if (!client.IsConnected)
+ return;
+
+ using (var writer = new StreamWriter(client))
+ {
+ writer.WriteLine(cmd);
+ writer.Flush();
+ }
+
+ if (OperatingSystem.IsWindows())
+ client.WaitForPipeDrain();
+ else
+ Thread.Sleep(1000);
+ }
+ }
+ catch
+ {
+ // IGNORE
+ }
+ }
+
+ public void Dispose()
+ {
+ _cancellationTokenSource?.Cancel();
+ _singletonLock?.Dispose();
+ }
+
+ private async void StartServer()
+ {
+ using var reader = new StreamReader(_server);
+
+ while (!_cancellationTokenSource.IsCancellationRequested)
+ {
+ try
+ {
+ await _server.WaitForConnectionAsync(_cancellationTokenSource.Token);
+
+ if (!_cancellationTokenSource.IsCancellationRequested)
+ {
+ var line = await reader.ReadToEndAsync(_cancellationTokenSource.Token);
+ MessageReceived?.Invoke(line?.Trim());
+ }
+
+ _server.Disconnect();
+ }
+ catch
+ {
+ if (!_cancellationTokenSource.IsCancellationRequested && _server.IsConnected)
+ _server.Disconnect();
+ }
+ }
+ }
+
+ private FileStream _singletonLock = null;
+ private NamedPipeServerStream _server = null;
+ private CancellationTokenSource _cancellationTokenSource = null;
+ }
+}
diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs
index 29487a16..40c84b9e 100644
--- a/src/Models/IssueTrackerRule.cs
+++ b/src/Models/IssueTrackerRule.cs
@@ -1,5 +1,4 @@
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
+using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -46,7 +45,7 @@ namespace SourceGit.Models
set => SetProperty(ref _urlTemplate, value);
}
- public void Matches(List outs, string message)
+ public void Matches(InlineElementCollector outs, string message)
{
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return;
@@ -60,17 +59,7 @@ namespace SourceGit.Models
var start = match.Index;
var len = match.Length;
- var intersect = false;
- foreach (var exist in outs)
- {
- if (exist.Intersect(start, len))
- {
- intersect = true;
- break;
- }
- }
-
- if (intersect)
+ if (outs.Intersect(start, len) != null)
continue;
var link = _urlTemplate;
@@ -81,8 +70,7 @@ namespace SourceGit.Models
link = link.Replace($"${j}", group.Value);
}
- var range = new Hyperlink(start, len, link);
- outs.Add(range);
+ outs.Add(new InlineElement(InlineElementType.Link, start, len, link));
}
}
diff --git a/src/Models/LFSObject.cs b/src/Models/LFSObject.cs
index 0f281253..8bc2dda2 100644
--- a/src/Models/LFSObject.cs
+++ b/src/Models/LFSObject.cs
@@ -1,8 +1,22 @@
-namespace SourceGit.Models
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Models
{
- public class LFSObject
+ public partial class LFSObject
{
+ [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")]
+ private static partial Regex REG_FORMAT();
+
public string Oid { get; set; } = string.Empty;
public long Size { get; set; } = 0;
+
+ public static LFSObject Parse(string content)
+ {
+ var match = REG_FORMAT().Match(content);
+ if (match.Success)
+ return new() { Oid = match.Groups[1].Value, Size = long.Parse(match.Groups[2].Value) };
+
+ return null;
+ }
}
}
diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs
index d5e1534c..1788a9b2 100644
--- a/src/Models/Locales.cs
+++ b/src/Models/Locales.cs
@@ -14,9 +14,12 @@ namespace SourceGit.Models
new Locale("Français", "fr_FR"),
new Locale("Italiano", "it_IT"),
new Locale("Português (Brasil)", "pt_BR"),
+ new Locale("Українська", "uk_UA"),
new Locale("Русский", "ru_RU"),
new Locale("简体中文", "zh_CN"),
new Locale("繁體中文", "zh_TW"),
+ new Locale("日本語", "ja_JP"),
+ new Locale("தமிழ் (Tamil)", "ta_IN"),
};
public Locale(string name, string key)
diff --git a/src/Models/MergeMode.cs b/src/Models/MergeMode.cs
index 15e3f7e9..5dc70030 100644
--- a/src/Models/MergeMode.cs
+++ b/src/Models/MergeMode.cs
@@ -5,8 +5,9 @@
public static readonly MergeMode[] Supported =
[
new MergeMode("Default", "Fast-forward if possible", ""),
+ new MergeMode("Fast-forward", "Refuse to merge when fast-forward is not possible", "--ff-only"),
new MergeMode("No Fast-forward", "Always create a merge commit", "--no-ff"),
- new MergeMode("Squash", "Use '--squash'", "--squash"),
+ new MergeMode("Squash", "Squash merge", "--squash"),
new MergeMode("Don't commit", "Merge without commit", "--no-ff --no-commit"),
];
diff --git a/src/Models/Null.cs b/src/Models/Null.cs
index e22ef8b3..1820d4d0 100644
--- a/src/Models/Null.cs
+++ b/src/Models/Null.cs
@@ -1,6 +1,4 @@
namespace SourceGit.Models
{
- public class Null
- {
- }
+ public class Null;
}
diff --git a/src/Models/NumericSort.cs b/src/Models/NumericSort.cs
index ed5002e6..baaf3da4 100644
--- a/src/Models/NumericSort.cs
+++ b/src/Models/NumericSort.cs
@@ -1,4 +1,6 @@
-namespace SourceGit.Models
+using System;
+
+namespace SourceGit.Models
{
public static class NumericSort
{
@@ -10,52 +12,35 @@
int marker1 = 0;
int marker2 = 0;
- char[] tmp1 = new char[len1];
- char[] tmp2 = new char[len2];
-
while (marker1 < len1 && marker2 < len2)
{
char c1 = s1[marker1];
char c2 = s2[marker2];
- int loc1 = 0;
- int loc2 = 0;
bool isDigit1 = char.IsDigit(c1);
bool isDigit2 = char.IsDigit(c2);
if (isDigit1 != isDigit2)
return c1.CompareTo(c2);
- do
- {
- tmp1[loc1] = c1;
- loc1++;
- marker1++;
+ int subLen1 = 1;
+ while (marker1 + subLen1 < len1 && char.IsDigit(s1[marker1 + subLen1]) == isDigit1)
+ subLen1++;
- if (marker1 < len1)
- c1 = s1[marker1];
- else
- break;
- } while (char.IsDigit(c1) == isDigit1);
+ int subLen2 = 1;
+ while (marker2 + subLen2 < len2 && char.IsDigit(s2[marker2 + subLen2]) == isDigit2)
+ subLen2++;
- do
- {
- tmp2[loc2] = c2;
- loc2++;
- marker2++;
+ string sub1 = s1.Substring(marker1, subLen1);
+ string sub2 = s2.Substring(marker2, subLen2);
- if (marker2 < len2)
- c2 = s2[marker2];
- else
- break;
- } while (char.IsDigit(c2) == isDigit2);
+ marker1 += subLen1;
+ marker2 += subLen2;
- string sub1 = new string(tmp1, 0, loc1);
- string sub2 = new string(tmp2, 0, loc2);
int result;
if (isDigit1)
- result = loc1 == loc2 ? string.CompareOrdinal(sub1, sub2) : loc1 - loc2;
+ result = (subLen1 == subLen2) ? string.CompareOrdinal(sub1, sub2) : (subLen1 - subLen2);
else
- result = string.CompareOrdinal(sub1, sub2);
+ result = string.Compare(sub1, sub2, StringComparison.OrdinalIgnoreCase);
if (result != 0)
return result;
diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs
index df67ff66..ab2a92b3 100644
--- a/src/Models/OpenAI.cs
+++ b/src/Models/OpenAI.cs
@@ -1,79 +1,99 @@
using System;
+using System.ClientModel;
using System.Collections.Generic;
-using System.Net.Http;
using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
using System.Threading;
-
+using Azure.AI.OpenAI;
using CommunityToolkit.Mvvm.ComponentModel;
+using OpenAI;
+using OpenAI.Chat;
namespace SourceGit.Models
{
- public class OpenAIChatMessage
+ public partial class OpenAIResponse
{
- [JsonPropertyName("role")]
- public string Role
+ public OpenAIResponse(Action onUpdate)
{
- get;
- set;
+ _onUpdate = onUpdate;
}
- [JsonPropertyName("content")]
- public string Content
+ public void Append(string text)
{
- get;
- set;
- }
- }
+ var buffer = text;
- public class OpenAIChatChoice
- {
- [JsonPropertyName("index")]
- public int Index
- {
- get;
- set;
+ if (_thinkTail.Length > 0)
+ {
+ _thinkTail.Append(buffer);
+ buffer = _thinkTail.ToString();
+ _thinkTail.Clear();
+ }
+
+ buffer = REG_COT().Replace(buffer, "");
+
+ var startIdx = buffer.IndexOf('<', StringComparison.Ordinal);
+ if (startIdx >= 0)
+ {
+ if (startIdx > 0)
+ OnReceive(buffer.Substring(0, startIdx));
+
+ var endIdx = buffer.IndexOf(">", startIdx + 1, StringComparison.Ordinal);
+ if (endIdx <= startIdx)
+ {
+ if (buffer.Length - startIdx <= 15)
+ _thinkTail.Append(buffer.Substring(startIdx));
+ else
+ OnReceive(buffer.Substring(startIdx));
+ }
+ else if (endIdx < startIdx + 15)
+ {
+ var tag = buffer.Substring(startIdx + 1, endIdx - startIdx - 1);
+ if (_thinkTags.Contains(tag))
+ _thinkTail.Append(buffer.Substring(startIdx));
+ else
+ OnReceive(buffer.Substring(startIdx));
+ }
+ else
+ {
+ OnReceive(buffer.Substring(startIdx));
+ }
+ }
+ else
+ {
+ OnReceive(buffer);
+ }
}
- [JsonPropertyName("message")]
- public OpenAIChatMessage Message
+ public void End()
{
- get;
- set;
- }
- }
-
- public class OpenAIChatResponse
- {
- [JsonPropertyName("choices")]
- public List Choices
- {
- get;
- set;
- } = [];
- }
-
- public class OpenAIChatRequest
- {
- [JsonPropertyName("model")]
- public string Model
- {
- get;
- set;
+ if (_thinkTail.Length > 0)
+ {
+ OnReceive(_thinkTail.ToString());
+ _thinkTail.Clear();
+ }
}
- [JsonPropertyName("messages")]
- public List Messages
+ private void OnReceive(string text)
{
- get;
- set;
- } = [];
+ if (!_hasTrimmedStart)
+ {
+ text = text.TrimStart();
+ if (string.IsNullOrEmpty(text))
+ return;
- public void AddMessage(string role, string content)
- {
- Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
+ _hasTrimmedStart = true;
+ }
+
+ _onUpdate.Invoke(text);
}
+
+ [GeneratedRegex(@"<(think|thought|thinking|thought_chain)>.*?\1>", RegexOptions.Singleline)]
+ private static partial Regex REG_COT();
+
+ private Action _onUpdate = null;
+ private StringBuilder _thinkTail = new StringBuilder();
+ private HashSet _thinkTags = ["think", "thought", "thinking", "thought_chain"];
+ private bool _hasTrimmedStart = false;
}
public class OpenAIService : ObservableObject
@@ -102,6 +122,12 @@ namespace SourceGit.Models
set => SetProperty(ref _model, value);
}
+ public bool Streaming
+ {
+ get => _streaming;
+ set => SetProperty(ref _streaming, value);
+ }
+
public string AnalyzeDiffPrompt
{
get => _analyzeDiffPrompt;
@@ -147,45 +173,46 @@ namespace SourceGit.Models
""";
}
- public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
+ public void Chat(string prompt, string question, CancellationToken cancellation, Action onUpdate)
{
- var chat = new OpenAIChatRequest() { Model = Model };
- chat.AddMessage("user", prompt);
- chat.AddMessage("user", question);
+ var server = new Uri(_server);
+ var key = new ApiKeyCredential(_apiKey);
+ var oaiClient = _server.Contains("openai.azure.com/", StringComparison.Ordinal)
+ ? new AzureOpenAIClient(server, key)
+ : new OpenAIClient(key, new() { Endpoint = server });
+ var client = oaiClient.GetChatClient(_model);
+ var messages = new List();
+ messages.Add(_model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt));
+ messages.Add(new UserChatMessage(question));
- var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
- if (!string.IsNullOrEmpty(ApiKey))
- {
- if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
- client.DefaultRequestHeaders.Add("api-key", ApiKey);
- else
- client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
- }
-
- var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
try
{
- var task = client.PostAsync(Server, req, cancellation);
- task.Wait(cancellation);
+ var rsp = new OpenAIResponse(onUpdate);
- var rsp = task.Result;
- var reader = rsp.Content.ReadAsStringAsync(cancellation);
- reader.Wait(cancellation);
-
- var body = reader.Result;
- if (!rsp.IsSuccessStatusCode)
+ if (_streaming)
{
- throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}");
+ var updates = client.CompleteChatStreaming(messages, null, cancellation);
+
+ foreach (var update in updates)
+ {
+ if (update.ContentUpdate.Count > 0)
+ rsp.Append(update.ContentUpdate[0].Text);
+ }
+ }
+ else
+ {
+ var completion = client.CompleteChat(messages, null, cancellation);
+
+ if (completion.Value.Content.Count > 0)
+ rsp.Append(completion.Value.Content[0].Text);
}
- return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
+ rsp.End();
}
catch
{
- if (cancellation.IsCancellationRequested)
- return null;
-
- throw;
+ if (!cancellation.IsCancellationRequested)
+ throw;
}
}
@@ -193,6 +220,7 @@ namespace SourceGit.Models
private string _server;
private string _apiKey;
private string _model;
+ private bool _streaming = true;
private string _analyzeDiffPrompt;
private string _generateSubjectPrompt;
}
diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs
index 3c452460..6e36cfb9 100644
--- a/src/Models/Remote.cs
+++ b/src/Models/Remote.cs
@@ -6,18 +6,21 @@ namespace SourceGit.Models
{
public partial class Remote
{
- [GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")]
+ [GeneratedRegex(@"^https?://[^/]+/.+[^/\.]$")]
private static partial Regex REG_HTTPS();
- [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.git)?$")]
+ [GeneratedRegex(@"^git://[^/]+/.+[^/\.]$")]
+ private static partial Regex REG_GIT();
+ [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
private static partial Regex REG_SSH1();
- [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~]+/[\w\-\.]+(\.git)?$")]
+ [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
private static partial Regex REG_SSH2();
- [GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~]+/[\w\-\.]+)\.git$")]
+ [GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~%]+/[\w\-\.%]+)\.git$")]
private static partial Regex REG_TO_VISIT_URL_CAPTURE();
private static readonly Regex[] URL_FORMATS = [
REG_HTTPS(),
+ REG_GIT(),
REG_SSH1(),
REG_SSH2(),
];
@@ -30,13 +33,10 @@ namespace SourceGit.Models
if (string.IsNullOrWhiteSpace(url))
return false;
- for (int i = 1; i < URL_FORMATS.Length; i++)
- {
- if (URL_FORMATS[i].IsMatch(url))
- return true;
- }
+ if (REG_SSH1().IsMatch(url))
+ return true;
- return false;
+ return REG_SSH2().IsMatch(url);
}
public static bool IsValidURL(string url)
@@ -50,7 +50,10 @@ namespace SourceGit.Models
return true;
}
- return url.EndsWith(".git", StringComparison.Ordinal) && Directory.Exists(url);
+ return url.StartsWith("file://", StringComparison.Ordinal) ||
+ url.StartsWith("./", StringComparison.Ordinal) ||
+ url.StartsWith("../", StringComparison.Ordinal) ||
+ Directory.Exists(url);
}
public bool TryGetVisitURL(out string url)
diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs
index 1d0b3c10..4e51b368 100644
--- a/src/Models/RepositorySettings.cs
+++ b/src/Models/RepositorySettings.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Text;
@@ -32,12 +32,24 @@ namespace SourceGit.Models
set;
} = false;
- public bool OnlyHighlighCurrentBranchInHistories
+ public bool OnlyHighlightCurrentBranchInHistories
{
get;
set;
} = false;
+ public BranchSortMode LocalBranchSortMode
+ {
+ get;
+ set;
+ } = BranchSortMode.Name;
+
+ public BranchSortMode RemoteBranchSortMode
+ {
+ get;
+ set;
+ } = BranchSortMode.Name;
+
public TagSortMode TagSortMode
{
get;
@@ -50,18 +62,6 @@ namespace SourceGit.Models
set;
} = true;
- public DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch
- {
- get;
- set;
- } = DealWithLocalChanges.DoNothing;
-
- public bool EnablePruneOnFetch
- {
- get;
- set;
- } = false;
-
public bool EnableForceOnFetch
{
get;
@@ -74,30 +74,12 @@ namespace SourceGit.Models
set;
} = false;
- public DealWithLocalChanges DealWithLocalChangesOnPull
- {
- get;
- set;
- } = DealWithLocalChanges.DoNothing;
-
public bool PreferRebaseInsteadOfMerge
{
get;
set;
} = true;
- public bool FetchWithoutTagsOnPull
- {
- get;
- set;
- } = false;
-
- public bool FetchAllBranchesOnPull
- {
- get;
- set;
- } = true;
-
public bool CheckSubmodulesOnPush
{
get;
@@ -110,11 +92,17 @@ namespace SourceGit.Models
set;
} = false;
- public DealWithLocalChanges DealWithLocalChangesOnCreateBranch
+ public bool PushToRemoteWhenCreateTag
{
get;
set;
- } = DealWithLocalChanges.DoNothing;
+ } = true;
+
+ public bool PushToRemoteWhenDeleteTag
+ {
+ get;
+ set;
+ } = false;
public bool CheckoutBranchOnCreateBranch
{
@@ -122,6 +110,12 @@ namespace SourceGit.Models
set;
} = true;
+ public bool UpdateSubmodulesOnCheckoutBranch
+ {
+ get;
+ set;
+ } = true;
+
public AvaloniaList HistoriesFilters
{
get;
@@ -182,13 +176,13 @@ namespace SourceGit.Models
set;
} = false;
- public bool KeepIndexWhenStash
+ public int ChangesAfterStashing
{
get;
set;
- } = false;
+ } = 0;
- public string PreferedOpenAIService
+ public string PreferredOpenAIService
{
get;
set;
@@ -230,6 +224,18 @@ namespace SourceGit.Models
set;
} = [];
+ public int PreferredMergeMode
+ {
+ get;
+ set;
+ } = 0;
+
+ public string LastCommitMessage
+ {
+ get;
+ set;
+ } = string.Empty;
+
public Dictionary CollectHistoriesFilters()
{
var map = new Dictionary();
@@ -275,9 +281,8 @@ namespace SourceGit.Models
return false;
}
- for (int i = 0; i < HistoriesFilters.Count; i++)
+ foreach (var filter in HistoriesFilters)
{
- var filter = HistoriesFilters[i];
if (filter.Type != type)
continue;
@@ -309,128 +314,81 @@ namespace SourceGit.Models
public string BuildHistoriesFilter()
{
+ var includedRefs = new List();
var excludedBranches = new List();
var excludedRemotes = new List();
var excludedTags = new List();
- var includedBranches = new List();
- var includedRemotes = new List();
- var includedTags = new List();
foreach (var filter in HistoriesFilters)
{
if (filter.Type == FilterType.LocalBranch)
{
- var name = filter.Pattern.Substring(11);
- var b = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]";
-
if (filter.Mode == FilterMode.Included)
- includedBranches.Add(b);
+ includedRefs.Add(filter.Pattern);
else if (filter.Mode == FilterMode.Excluded)
- excludedBranches.Add(b);
+ excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\"");
}
else if (filter.Type == FilterType.LocalBranchFolder)
{
if (filter.Mode == FilterMode.Included)
- includedBranches.Add($"{filter.Pattern.Substring(11)}/*");
+ includedRefs.Add($"--branches={filter.Pattern.AsSpan(11)}/*");
else if (filter.Mode == FilterMode.Excluded)
- excludedBranches.Add($"{filter.Pattern.Substring(11)}/*");
+ excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\"");
}
else if (filter.Type == FilterType.RemoteBranch)
{
- var name = filter.Pattern.Substring(13);
- var r = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]";
-
if (filter.Mode == FilterMode.Included)
- includedRemotes.Add(r);
+ includedRefs.Add(filter.Pattern);
else if (filter.Mode == FilterMode.Excluded)
- excludedRemotes.Add(r);
+ excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\"");
}
else if (filter.Type == FilterType.RemoteBranchFolder)
{
if (filter.Mode == FilterMode.Included)
- includedRemotes.Add($"{filter.Pattern.Substring(13)}/*");
+ includedRefs.Add($"--remotes={filter.Pattern.AsSpan(13)}/*");
else if (filter.Mode == FilterMode.Excluded)
- excludedRemotes.Add($"{filter.Pattern.Substring(13)}/*");
+ excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\"");
}
else if (filter.Type == FilterType.Tag)
{
- var name = filter.Pattern;
- var t = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]";
-
if (filter.Mode == FilterMode.Included)
- includedTags.Add(t);
+ includedRefs.Add($"refs/tags/{filter.Pattern}");
else if (filter.Mode == FilterMode.Excluded)
- excludedTags.Add(t);
+ excludedTags.Add($"--exclude=\"{filter.Pattern}\" --decorate-refs-exclude=\"refs/tags/{filter.Pattern}\"");
}
}
- bool hasIncluded = includedBranches.Count > 0 || includedRemotes.Count > 0 || includedTags.Count > 0;
- bool hasExcluded = excludedBranches.Count > 0 || excludedRemotes.Count > 0 || excludedTags.Count > 0;
-
var builder = new StringBuilder();
- if (hasIncluded)
+ if (includedRefs.Count > 0)
{
- foreach (var b in includedBranches)
+ foreach (var r in includedRefs)
+ {
+ builder.Append(r);
+ builder.Append(' ');
+ }
+ }
+ else if (excludedBranches.Count + excludedRemotes.Count + excludedTags.Count > 0)
+ {
+ foreach (var b in excludedBranches)
{
- builder.Append("--branches=");
builder.Append(b);
builder.Append(' ');
}
- foreach (var r in includedRemotes)
+ builder.Append("--exclude=HEAD --branches ");
+
+ foreach (var r in excludedRemotes)
{
- builder.Append("--remotes=");
builder.Append(r);
builder.Append(' ');
}
- foreach (var t in includedTags)
+ builder.Append("--exclude=origin/HEAD --remotes ");
+
+ foreach (var t in excludedTags)
{
- builder.Append("--tags=");
builder.Append(t);
builder.Append(' ');
}
- }
- else if (hasExcluded)
- {
- if (excludedBranches.Count > 0)
- {
- foreach (var b in excludedBranches)
- {
- builder.Append("--exclude=");
- builder.Append(b);
- builder.Append(" --decorate-refs-exclude=refs/heads/");
- builder.Append(b);
- builder.Append(' ');
- }
- }
-
- builder.Append("--exclude=HEA[D] --branches ");
-
- if (excludedRemotes.Count > 0)
- {
- foreach (var r in excludedRemotes)
- {
- builder.Append("--exclude=");
- builder.Append(r);
- builder.Append(" --decorate-refs-exclude=refs/remotes/");
- builder.Append(r);
- builder.Append(' ');
- }
- }
-
- builder.Append("--exclude=origin/HEA[D] --remotes ");
-
- if (excludedTags.Count > 0)
- {
- foreach (var t in excludedTags)
- {
- builder.Append("--exclude=");
- builder.Append(t);
- builder.Append(" --decorate-refs-exclude=refs/tags/");
- builder.Append(t);
- builder.Append(' ');
- }
- }
builder.Append("--tags ");
}
@@ -440,6 +398,7 @@ namespace SourceGit.Models
public void PushCommitMessage(string message)
{
+ message = message.Trim().ReplaceLineEndings("\n");
var existIdx = CommitMessages.IndexOf(message);
if (existIdx == 0)
return;
@@ -477,11 +436,7 @@ namespace SourceGit.Models
public CustomAction AddNewCustomAction()
{
- var act = new CustomAction()
- {
- Name = "Unnamed Custom Action",
- };
-
+ var act = new CustomAction() { Name = "Unnamed Action" };
CustomActions.Add(act);
return act;
}
@@ -491,5 +446,19 @@ namespace SourceGit.Models
if (act != null)
CustomActions.Remove(act);
}
+
+ public void MoveCustomActionUp(CustomAction act)
+ {
+ var idx = CustomActions.IndexOf(act);
+ if (idx > 0)
+ CustomActions.Move(idx - 1, idx);
+ }
+
+ public void MoveCustomActionDown(CustomAction act)
+ {
+ var idx = CustomActions.IndexOf(act);
+ if (idx < CustomActions.Count - 1)
+ CustomActions.Move(idx + 1, idx);
+ }
}
}
diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs
index f1f5265f..29a23efa 100644
--- a/src/Models/RevisionFile.cs
+++ b/src/Models/RevisionFile.cs
@@ -1,4 +1,6 @@
-using Avalonia.Media.Imaging;
+using System.Globalization;
+using System.IO;
+using Avalonia.Media.Imaging;
namespace SourceGit.Models
{
@@ -9,10 +11,17 @@ namespace SourceGit.Models
public class RevisionImageFile
{
- public Bitmap Image { get; set; } = null;
- public long FileSize { get; set; } = 0;
- public string ImageType { get; set; } = string.Empty;
+ public Bitmap Image { get; }
+ public long FileSize { get; }
+ public string ImageType { get; }
public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0";
+
+ public RevisionImageFile(string file, Bitmap img, long size)
+ {
+ Image = img;
+ FileSize = size;
+ ImageType = Path.GetExtension(file)!.Substring(1).ToUpper(CultureInfo.CurrentCulture);
+ }
}
public class RevisionTextFile
@@ -29,6 +38,6 @@ namespace SourceGit.Models
public class RevisionSubmodule
{
public Commit Commit { get; set; } = null;
- public string FullMessage { get; set; } = string.Empty;
+ public CommitFullMessage FullMessage { get; set; } = null;
}
}
diff --git a/src/Models/ScanDir.cs b/src/Models/ScanDir.cs
new file mode 100644
index 00000000..eb78a79c
--- /dev/null
+++ b/src/Models/ScanDir.cs
@@ -0,0 +1,8 @@
+namespace SourceGit.Models
+{
+ public record ScanDir(string path, string desc)
+ {
+ public string Path { get; set; } = path;
+ public string Desc { get; set; } = desc;
+ }
+}
diff --git a/src/Models/Version.cs b/src/Models/SelfUpdate.cs
similarity index 66%
rename from src/Models/Version.cs
rename to src/Models/SelfUpdate.cs
index 35c21778..05fa6124 100644
--- a/src/Models/Version.cs
+++ b/src/Models/SelfUpdate.cs
@@ -1,4 +1,5 @@
-using System.Reflection;
+using System;
+using System.Reflection;
using System.Text.Json.Serialization;
namespace SourceGit.Models
@@ -32,5 +33,22 @@ namespace SourceGit.Models
}
}
- public class AlreadyUpToDate { }
+ public class AlreadyUpToDate;
+
+ public class SelfUpdateFailed
+ {
+ public string Reason
+ {
+ get;
+ private set;
+ }
+
+ public SelfUpdateFailed(Exception e)
+ {
+ if (e.InnerException is { } inner)
+ Reason = inner.Message;
+ else
+ Reason = e.Message;
+ }
+ }
}
diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs
index 1decdcfa..7dfb2237 100644
--- a/src/Models/ShellOrTerminal.cs
+++ b/src/Models/ShellOrTerminal.cs
@@ -42,6 +42,8 @@ namespace SourceGit.Models
new ShellOrTerminal("mac-terminal", "Terminal", ""),
new ShellOrTerminal("iterm2", "iTerm", ""),
new ShellOrTerminal("warp", "Warp", ""),
+ new ShellOrTerminal("ghostty", "Ghostty", ""),
+ new ShellOrTerminal("kitty", "kitty", "")
};
}
else
@@ -56,6 +58,8 @@ namespace SourceGit.Models
new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"),
new ShellOrTerminal("foot", "Foot", "foot"),
new ShellOrTerminal("wezterm", "WezTerm", "wezterm"),
+ new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"),
+ new ShellOrTerminal("kitty", "kitty", "kitty"),
new ShellOrTerminal("custom", "Custom", ""),
};
}
diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs
index 8dca3bdb..2b77be50 100644
--- a/src/Models/Stash.cs
+++ b/src/Models/Stash.cs
@@ -10,8 +10,25 @@ namespace SourceGit.Models
public List Parents { get; set; } = [];
public ulong Time { get; set; } = 0;
public string Message { get; set; } = "";
- public bool HasUntracked => Parents.Count == 3;
- public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
+ public string Subject
+ {
+ get
+ {
+ var idx = Message.IndexOf('\n', StringComparison.Ordinal);
+ return idx > 0 ? Message.Substring(0, idx).Trim() : Message;
+ }
+ }
+
+ public string TimeStr
+ {
+ get
+ {
+ return DateTime.UnixEpoch
+ .AddSeconds(Time)
+ .ToLocalTime()
+ .ToString(DateTimeFormat.Active.DateTime);
+ }
+ }
}
}
diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs
index 969d3945..a86380c3 100644
--- a/src/Models/Statistics.cs
+++ b/src/Models/Statistics.cs
@@ -11,54 +11,47 @@ using SkiaSharp;
namespace SourceGit.Models
{
- public enum StaticsticsMode
+ public enum StatisticsMode
{
All,
ThisMonth,
ThisWeek,
}
- public class StaticsticsAuthor(string name, int count)
+ public class StatisticsAuthor(User user, int count)
{
- public string Name { get; set; } = name;
- public int Count { get; set; } = count;
- }
-
- public class StaticsticsSample(DateTime time, int count)
- {
- public DateTime Time { get; set; } = time;
+ public User User { get; set; } = user;
public int Count { get; set; } = count;
}
public class StatisticsReport
{
- public static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
-
public int Total { get; set; } = 0;
- public List Authors { get; set; } = new List();
- public List Series { get; set; } = new List();
- public List XAxes { get; set; } = new List();
- public List YAxes { get; set; } = new List();
+ public List Authors { get; set; } = new();
+ public List Series { get; set; } = new();
+ public List XAxes { get; set; } = new();
+ public List YAxes { get; set; } = new();
+ public StatisticsAuthor SelectedAuthor { get => _selectedAuthor; set => ChangeAuthor(value); }
- public StatisticsReport(StaticsticsMode mode, DateTime start)
+ public StatisticsReport(StatisticsMode mode, DateTime start)
{
_mode = mode;
- YAxes = [new Axis()
+ YAxes.Add(new Axis()
{
TextSize = 10,
MinLimit = 0,
SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 }
- }];
+ });
- if (mode == StaticsticsMode.ThisWeek)
+ if (mode == StatisticsMode.ThisWeek)
{
for (int i = 0; i < 7; i++)
_mapSamples.Add(start.AddDays(i), 0);
XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => WEEKDAYS[(int)v.DayOfWeek]) { TextSize = 10 });
}
- else if (mode == StaticsticsMode.ThisMonth)
+ else if (mode == StatisticsMode.ThisMonth)
{
var now = DateTime.Now;
var maxDays = DateTime.DaysInMonth(now.Year, now.Month);
@@ -73,12 +66,12 @@ namespace SourceGit.Models
}
}
- public void AddCommit(DateTime time, string author)
+ public void AddCommit(DateTime time, User author)
{
Total++;
- var normalized = DateTime.MinValue;
- if (_mode == StaticsticsMode.ThisWeek || _mode == StaticsticsMode.ThisMonth)
+ DateTime normalized;
+ if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth)
normalized = time.Date;
else
normalized = new DateTime(time.Year, time.Month, 1).ToLocalTime();
@@ -92,10 +85,30 @@ namespace SourceGit.Models
_mapUsers[author] = vu + 1;
else
_mapUsers.Add(author, 1);
+
+ if (_mapUserSamples.TryGetValue(author, out var vus))
+ {
+ if (vus.TryGetValue(normalized, out var n))
+ vus[normalized] = n + 1;
+ else
+ vus.Add(normalized, 1);
+ }
+ else
+ {
+ _mapUserSamples.Add(author, new Dictionary
+ {
+ { normalized, 1 }
+ });
+ }
}
public void Complete()
{
+ foreach (var kv in _mapUsers)
+ Authors.Add(new StatisticsAuthor(kv.Key, kv.Value));
+
+ Authors.Sort((l, r) => r.Count - l.Count);
+
var samples = new List();
foreach (var kv in _mapSamples)
samples.Add(new DateTimePoint(kv.Key, kv.Value));
@@ -110,65 +123,111 @@ namespace SourceGit.Models
}
);
- foreach (var kv in _mapUsers)
- Authors.Add(new StaticsticsAuthor(kv.Key, kv.Value));
-
- Authors.Sort((l, r) => r.Count - l.Count);
-
_mapUsers.Clear();
_mapSamples.Clear();
}
public void ChangeColor(uint color)
{
- if (Series is [ColumnSeries series])
- series.Fill = new SolidColorPaint(new SKColor(color));
+ _fillColor = color;
+
+ var fill = new SKColor(color);
+
+ if (Series.Count > 0 && Series[0] is ColumnSeries total)
+ total.Fill = new SolidColorPaint(_selectedAuthor == null ? fill : fill.WithAlpha(51));
+
+ if (Series.Count > 1 && Series[1] is ColumnSeries user)
+ user.Fill = new SolidColorPaint(fill);
}
- private StaticsticsMode _mode = StaticsticsMode.All;
- private Dictionary _mapUsers = new Dictionary();
- private Dictionary _mapSamples = new Dictionary();
+ public void ChangeAuthor(StatisticsAuthor author)
+ {
+ if (author == _selectedAuthor)
+ return;
+
+ _selectedAuthor = author;
+ Series.RemoveRange(1, Series.Count - 1);
+ if (author == null || !_mapUserSamples.TryGetValue(author.User, out var userSamples))
+ {
+ ChangeColor(_fillColor);
+ return;
+ }
+
+ var samples = new List();
+ foreach (var kv in userSamples)
+ samples.Add(new DateTimePoint(kv.Key, kv.Value));
+
+ Series.Add(
+ new ColumnSeries()
+ {
+ Values = samples,
+ Stroke = null,
+ Fill = null,
+ Padding = 1,
+ }
+ );
+
+ ChangeColor(_fillColor);
+ }
+
+ private static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
+ private StatisticsMode _mode;
+ private Dictionary _mapUsers = new();
+ private Dictionary _mapSamples = new();
+ private Dictionary> _mapUserSamples = new();
+ private StatisticsAuthor _selectedAuthor = null;
+ private uint _fillColor = 255;
}
public class Statistics
{
- public StatisticsReport All { get; set; }
- public StatisticsReport Month { get; set; }
- public StatisticsReport Week { get; set; }
+ public StatisticsReport All { get; }
+ public StatisticsReport Month { get; }
+ public StatisticsReport Week { get; }
public Statistics()
{
- _today = DateTime.Now.ToLocalTime().Date;
- var weekOffset = (7 + (int)_today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7;
- _thisWeekStart = _today.AddDays(-weekOffset);
- _thisMonthStart = _today.AddDays(1 - _today.Day);
+ var today = DateTime.Now.ToLocalTime().Date;
+ var weekOffset = (7 + (int)today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7;
+ _thisWeekStart = today.AddDays(-weekOffset);
+ _thisMonthStart = today.AddDays(1 - today.Day);
- All = new StatisticsReport(StaticsticsMode.All, DateTime.MinValue);
- Month = new StatisticsReport(StaticsticsMode.ThisMonth, _thisMonthStart);
- Week = new StatisticsReport(StaticsticsMode.ThisWeek, _thisWeekStart);
+ All = new StatisticsReport(StatisticsMode.All, DateTime.MinValue);
+ Month = new StatisticsReport(StatisticsMode.ThisMonth, _thisMonthStart);
+ Week = new StatisticsReport(StatisticsMode.ThisWeek, _thisWeekStart);
}
public void AddCommit(string author, double timestamp)
{
+ var emailIdx = author.IndexOf('±', StringComparison.Ordinal);
+ var email = author.Substring(emailIdx + 1).ToLower(CultureInfo.CurrentCulture);
+ if (!_users.TryGetValue(email, out var user))
+ {
+ user = User.FindOrAdd(author);
+ _users.Add(email, user);
+ }
+
var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
if (time >= _thisWeekStart)
- Week.AddCommit(time, author);
+ Week.AddCommit(time, user);
if (time >= _thisMonthStart)
- Month.AddCommit(time, author);
+ Month.AddCommit(time, user);
- All.AddCommit(time, author);
+ All.AddCommit(time, user);
}
public void Complete()
{
+ _users.Clear();
+
All.Complete();
Month.Complete();
Week.Complete();
}
- private readonly DateTime _today;
private readonly DateTime _thisMonthStart;
private readonly DateTime _thisWeekStart;
+ private readonly Dictionary _users = new();
}
}
diff --git a/src/Models/Submodule.cs b/src/Models/Submodule.cs
index ce00ac02..ca73a8de 100644
--- a/src/Models/Submodule.cs
+++ b/src/Models/Submodule.cs
@@ -1,8 +1,20 @@
namespace SourceGit.Models
{
+ public enum SubmoduleStatus
+ {
+ Normal = 0,
+ NotInited,
+ RevisionChanged,
+ Unmerged,
+ Modified,
+ }
+
public class Submodule
{
- public string Path { get; set; } = "";
- public bool IsDirty { get; set; } = false;
+ public string Path { get; set; } = string.Empty;
+ public string SHA { get; set; } = string.Empty;
+ public string URL { get; set; } = string.Empty;
+ public SubmoduleStatus Status { get; set; } = SubmoduleStatus.Normal;
+ public bool IsDirty => Status > SubmoduleStatus.NotInited;
}
}
diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs
index 51681d93..87944637 100644
--- a/src/Models/Tag.cs
+++ b/src/Models/Tag.cs
@@ -5,13 +5,13 @@ namespace SourceGit.Models
public enum TagSortMode
{
CreatorDate = 0,
- NameInAscending,
- NameInDescending,
+ Name,
}
public class Tag : ObservableObject
{
public string Name { get; set; } = string.Empty;
+ public bool IsAnnotated { get; set; } = false;
public string SHA { get; set; } = string.Empty;
public ulong CreatorDate { get; set; } = 0;
public string Message { get; set; } = string.Empty;
diff --git a/src/Models/TemplateEngine.cs b/src/Models/TemplateEngine.cs
index 6b5f525d..8f60bd74 100644
--- a/src/Models/TemplateEngine.cs
+++ b/src/Models/TemplateEngine.cs
@@ -102,7 +102,7 @@ namespace SourceGit.Models
private int? Integer()
{
var start = _pos;
- while (Peek() is char c && c >= '0' && c <= '9')
+ while (Peek() is >= '0' and <= '9')
{
_pos++;
}
@@ -118,7 +118,7 @@ namespace SourceGit.Models
// text token start
var tok = _pos;
bool esc = false;
- while (Next() is char c)
+ while (Next() is { } c)
{
if (esc)
{
@@ -129,7 +129,7 @@ namespace SourceGit.Models
{
case ESCAPE:
// allow to escape only \ and $
- if (Peek() is char nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR))
+ if (Peek() is { } nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR))
{
esc = true;
FlushText(tok, _pos - 1);
@@ -173,7 +173,7 @@ namespace SourceGit.Models
if (Next() != VARIABLE_START)
return null;
int name_start = _pos;
- while (Next() is char c)
+ while (Next() is { } c)
{
// name character, continue advancing
if (IsNameChar(c))
@@ -228,7 +228,7 @@ namespace SourceGit.Models
var sb = new StringBuilder();
var tok = _pos;
var esc = false;
- while (Next() is char c)
+ while (Next() is { } c)
{
if (esc)
{
@@ -277,7 +277,7 @@ namespace SourceGit.Models
var sb = new StringBuilder();
var tok = _pos;
var esc = false;
- while (Next() is char c)
+ while (Next() is { } c)
{
if (esc)
{
@@ -313,7 +313,7 @@ namespace SourceGit.Models
private static bool IsNameChar(char c)
{
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
// (?) notice or log if variable is not found
@@ -402,7 +402,7 @@ namespace SourceGit.Models
sb.AppendJoin(", ", paths);
if (max < context.changes.Count)
- sb.AppendFormat(" and {0} other files", context.changes.Count - max);
+ sb.Append($" and {context.changes.Count - max} other files");
return sb.ToString();
}
diff --git a/src/Models/TextInlineChange.cs b/src/Models/TextInlineChange.cs
index c96d839f..afe5bec4 100644
--- a/src/Models/TextInlineChange.cs
+++ b/src/Models/TextInlineChange.cs
@@ -2,30 +2,22 @@
namespace SourceGit.Models
{
- public class TextInlineChange
+ public class TextInlineChange(int dp, int dc, int ap, int ac)
{
- public int DeletedStart { get; set; }
- public int DeletedCount { get; set; }
- public int AddedStart { get; set; }
- public int AddedCount { get; set; }
+ public int DeletedStart { get; set; } = dp;
+ public int DeletedCount { get; set; } = dc;
+ public int AddedStart { get; set; } = ap;
+ public int AddedCount { get; set; } = ac;
- class Chunk
+ private class Chunk(int hash, int start, int size)
{
- public int Hash;
+ public readonly int Hash = hash;
+ public readonly int Start = start;
+ public readonly int Size = size;
public bool Modified;
- public int Start;
- public int Size;
-
- public Chunk(int hash, int start, int size)
- {
- Hash = hash;
- Modified = false;
- Start = start;
- Size = size;
- }
}
- enum Edit
+ private enum Edit
{
None,
DeletedRight,
@@ -34,7 +26,7 @@ namespace SourceGit.Models
AddedLeft,
}
- class EditResult
+ private class EditResult
{
public Edit State;
public int DeleteStart;
@@ -43,14 +35,6 @@ namespace SourceGit.Models
public int AddEnd;
}
- public TextInlineChange(int dp, int dc, int ap, int ac)
- {
- DeletedStart = dp;
- DeletedCount = dc;
- AddedStart = ap;
- AddedCount = ac;
- }
-
public static List Compare(string oldValue, string newValue)
{
var hashes = new Dictionary();
@@ -204,11 +188,10 @@ namespace SourceGit.Models
for (int i = 0; i <= half; i++)
{
-
for (int j = -i; j <= i; j += 2)
{
var idx = j + half;
- int o, n;
+ int o;
if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1]))
{
o = forward[idx + 1];
@@ -220,7 +203,7 @@ namespace SourceGit.Models
rs.State = Edit.DeletedRight;
}
- n = o - j;
+ var n = o - j;
var startX = o;
var startY = n;
@@ -258,7 +241,7 @@ namespace SourceGit.Models
for (int j = -i; j <= i; j += 2)
{
var idx = j + half;
- int o, n;
+ int o;
if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1]))
{
o = reverse[idx + 1] - 1;
@@ -270,7 +253,7 @@ namespace SourceGit.Models
rs.State = Edit.AddedLeft;
}
- n = o - (j + delta);
+ var n = o - (j + delta);
var endX = o;
var endY = n;
@@ -312,8 +295,7 @@ namespace SourceGit.Models
private static void AddChunk(List