This commit is contained in:
Bernat Borràs Civil 2025-04-11 16:50:28 +02:00
commit 75b15c455e
284 changed files with 10116 additions and 5764 deletions

View file

@ -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 zlib1g-dev:arm64
- name: Build
run: dotnet build -c Release
- name: Publish

View file

@ -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

View file

@ -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.
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -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
@ -40,6 +40,7 @@
* Git LFS
* Issue Link
* Workspace
* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING]
@ -47,10 +48,7 @@
## Translation Status
[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-97.55%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-100.00%25-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-92.79%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-93.33%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-94.42%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-100.00%25-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](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
@ -62,7 +60,7 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD
| OS | PATH |
|---------|-----------------------------------------------------|
| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` |
| Windows | `%APPDATA%\SourceGit` |
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
| macOS | `${HOME}/Library/Application Support/SourceGit` |
@ -79,7 +77,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
@ -107,7 +105,7 @@ For **Linux** users:
`deb` how to:
```shell
curl https://codeberg.org/api/packages/yataro/debian/repository.key | sudo tee /etc/apt/keyrings/sourcegit.asc
echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc] https://codeberg.org/api/packages/yataro/debian generic main" | sudo tee /etc/apt/sources.list.d/sourcegit.list
echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc, arch=amd64,arm64] https://codeberg.org/api/packages/yataro/debian generic main" | sudo tee /etc/apt/sources.list.d/sourcegit.list
sudo apt update
sudo apt install sourcegit
```
@ -132,15 +130,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 +199,7 @@ dotnet run --project src/SourceGit.csproj
Thanks to all the people who contribute.
[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=20)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
## Third-Party Components
For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md).

View file

@ -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}"

86
THIRD-PARTY-LICENSES.md Normal file
View file

@ -0,0 +1,86 @@
# Third-Party Licenses
This project incorporates components from the following third parties:
## Packages
### AvaloniaUI
- **Source**: https://github.com/AvaloniaUI/Avalonia
- **Version**: 11.2.5
- **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-beta2
- **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-beta2
- **License**: MIT License
- **License Link**: https://github.com/Azure/azure-sdk-for-net/blob/main/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

View file

@ -1,55 +1,150 @@
### de_DE.axaml: 97.55%
# Translation Status
This document shows the translation status of each locale file in the repository.
## Details
### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)
### ![de__DE](https://img.shields.io/badge/de__DE-97.34%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in de_DE.axaml</summary>
- Text.Configure.IssueTracker.AddSampleGiteeIssue
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
- Text.Preference.General.DateFormat
- Text.Preference.Git.SSLVerify
- Text.Repository.HistoriesLayout
- Text.Repository.HistoriesLayout.Horizontal
- Text.Repository.HistoriesLayout.Vertical
- Text.Repository.HistoriesOrder
- Text.Repository.OnlyHighlightCurrentBranchInHistories
- Text.Repository.Tags.OrderByCreatorDate
- Text.Repository.Tags.OrderByNameAsc
- Text.BranchUpstreamInvalid
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.Git.PreferredMergeMode
- Text.Configure.IssueTracker.AddSampleAzure
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.Diff.First
- Text.Diff.Last
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.ShowTagsInGraph
- Text.StashCM.SaveAsPatch
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### ![es__ES](https://img.shields.io/badge/es__ES-98.67%25-yellow)
<details>
<summary>Missing keys in es_ES.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### ![fr__FR](https://img.shields.io/badge/fr__FR-98.67%25-yellow)
<details>
<summary>Missing keys in fr_FR.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### ![it__IT](https://img.shields.io/badge/it__IT-98.40%25-yellow)
<details>
<summary>Missing keys in it_IT.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.Preferences.General.ShowTagsInGraph
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### ![ja__JP](https://img.shields.io/badge/ja__JP-98.40%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.Repository.FilterCommits
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories
- Text.SetUpstream
- Text.SetUpstream.Local
- Text.SetUpstream.Unset
- Text.SetUpstream.Upstream
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### es_ES.axaml: 100.00%
### ![pt__BR](https://img.shields.io/badge/pt__BR-89.76%25-yellow)
<details>
<summary>Missing Keys</summary>
</details>
### fr_FR.axaml: 92.79%
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in pt_BR.axaml</summary>
- Text.AIAssistant.Regen
- Text.AIAssistant.Use
- Text.ApplyStash
- Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash
- Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
- Text.CherryPick.AppendSourceToMessage
- Text.CherryPick.Mainline.Tips
- Text.CommitCM.CherryPickMultiple
- Text.BranchUpstreamInvalid
- Text.Clone.RecurseSubmodules
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.Configure.CustomAction.Scope.Branch
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.Git.PreferredMergeMode
- Text.Configure.IssueTracker.AddSampleGiteeIssue
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.CreateBranch.Name.WarnSpace
- Text.DeleteRepositoryNode.Path
- Text.DeleteRepositoryNode.TipForGroup
- Text.DeleteRepositoryNode.TipForRepository
- Text.Diff.First
- Text.Diff.Last
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
@ -63,132 +158,18 @@
- Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets
- Text.Preference.Appearance.FontSize
- Text.Preference.Appearance.FontSize.Default
- Text.Preference.Appearance.FontSize.Editor
- Text.Preference.General.DateFormat
- Text.Preference.General.ShowChildren
- Text.Preference.Git.SSLVerify
- Text.Repository.CustomActions
- 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.ScanRepositories
- Text.SetUpstream
- Text.SetUpstream.Local
- Text.SetUpstream.Unset
- Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo
- Text.WorkingCopy.CommitToEdit
</details>
### it_IT.axaml: 93.33%
<details>
<summary>Missing Keys</summary>
- 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.Preference.General.DateFormat
- Text.Preference.General.ShowChildren
- Text.Preference.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
</details>
### pt_BR.axaml: 94.42%
<details>
<summary>Missing Keys</summary>
- 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.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.Hotkeys.Global.Clone
- 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.Preference.General.DateFormat
- Text.Preference.General.ShowChildren
- Text.Preference.Git.SSLVerify
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph
- Text.Preferences.Git.SSLVerify
- 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.Skip
- Text.Repository.Tags.OrderByCreatorDate
@ -201,36 +182,55 @@
- Text.SetUpstream.Unset
- Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo
- Text.Stash.AutoRestore
- Text.Stash.AutoRestore.Tip
- Text.StashCM.SaveAsPatch
- Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.SignOff
</details>
### ru_RU.axaml: 100.00%
### ![ru__RU](https://img.shields.io/badge/ru__RU-98.67%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in ru_RU.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### zh_CN.axaml: 100.00%
### ![ta__IN](https://img.shields.io/badge/ta__IN-98.67%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in ta_IN.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.UpdateSubmodules.Target
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### zh_TW.axaml: 100.00%
### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)
<details>
<summary>Missing Keys</summary>
</details>
### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)

View file

@ -1 +1 @@
2025.01
2025.12

View file

@ -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

View file

@ -0,0 +1,32 @@
#!/bin/sh
set -e
# summary of how this script can be called:
# * <new-preinst> `install'
# * <new-preinst> `install' <old-version>
# * <new-preinst> `upgrade' <old-version>
# * <old-preinst> `abort-upgrade' <new-version>
# 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

View file

@ -0,0 +1,35 @@
#!/bin/sh
set -e
# summary of how this script can be called:
# * <prerm> `remove'
# * <old-prerm> `upgrade' <new-version>
# * <new-prerm> `failed-upgrade' <old-version>
# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
# * <deconfigured's-prerm> `deconfigure' `in-favour'
# <package-being-installed> <version> `removing'
# <conflicting-package> <version>
# 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

View file

@ -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();
@ -18,42 +17,36 @@ async function parseXml(filePath) {
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(`[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](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(`### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)`);
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(`<details>\n<summary>Missing Keys</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`);
if (missingKeys.length > 0) {
const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
const badgeColor = progress >= 75 ? 'yellow' : 'red';
// Add badges
const locale = file.replace('.axaml', '').replace('_', '__');
const badgeColor = translationRate === 100 ? 'brightgreen' : translationRate >= 75 ? 'yellow' : 'red';
badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-${translationRate.toFixed(2)}%25-${badgeColor})](TRANSLATION.md)`);
lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-${progress.toFixed(2)}%25-${badgeColor})`);
lines.push(`<details>\n<summary>Missing keys in ${file}</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`)
} else {
lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)`);
}
}
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));

View file

@ -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" ./

View file

@ -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

View file

@ -37,7 +37,7 @@ namespace SourceGit
}
}
public static readonly Command OpenPreferenceCommand = new Command(_ => OpenDialog(new Views.Preference()));
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 OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));

View file

@ -46,11 +46,9 @@ 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))]
[JsonSerializable(typeof(ViewModels.Preference))]
[JsonSerializable(typeof(ViewModels.Preferences))]
internal partial class JsonCodeGen : JsonSerializerContext { }
}

View file

@ -20,6 +20,8 @@
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/>
<ResourceInclude x:Key="zh_TW" Source="/Resources/Locales/zh_TW.axaml"/>
<ResourceInclude x:Key="es_ES" Source="/Resources/Locales/es_ES.axaml"/>
<ResourceInclude x:Key="ja_JP" Source="/Resources/Locales/ja_JP.axaml"/>
<ResourceInclude x:Key="ta_IN" Source="/Resources/Locales/ta_IN.axaml"/>
</ResourceDictionary>
</Application.Resources>
@ -35,7 +37,7 @@
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preference}" Command="{x:Static s:App.OpenPreferenceCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>

View file

@ -1,10 +1,12 @@
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.Threading;
using System.Threading.Tasks;
using Avalonia;
@ -35,15 +37,14 @@ namespace SourceGit
TaskScheduler.UnobservedTaskException += (_, e) =>
{
LogException(e.Exception);
e.SetObserved();
};
try
{
if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo))
if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo))
Environment.Exit(exitTodo);
else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage))
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
Environment.Exit(exitMessage);
else
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
@ -75,6 +76,31 @@ namespace SourceGit
Native.OS.SetupApp(builder);
return builder;
}
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}\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
@ -179,6 +205,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));
@ -298,7 +327,7 @@ namespace SourceGit
{
AvaloniaXamlLoader.Load(this);
var pref = ViewModels.Preference.Instance;
var pref = ViewModels.Preferences.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
SetLocale(pref.Locale);
@ -312,44 +341,18 @@ namespace SourceGit
{
BindingPlugins.DataValidators.RemoveAt(0);
if (TryLaunchedAsCoreEditor(desktop))
if (TryLaunchAsCoreEditor(desktop))
return;
if (TryLaunchedAsAskpass(desktop))
if (TryLaunchAsAskpass(desktop))
return;
TryLaunchedAsNormal(desktop);
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 TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode)
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -402,7 +405,7 @@ namespace SourceGit
return true;
}
private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode)
private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -436,7 +439,7 @@ namespace SourceGit
return true;
}
private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{
var args = desktop.Args;
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
@ -444,14 +447,18 @@ namespace SourceGit
var file = args[1];
if (!File.Exists(file))
{
desktop.Shutdown(-1);
else
desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
return true;
}
var editor = new Views.StandaloneCommitMessageEditor();
editor.SetFile(file);
desktop.MainWindow = editor;
return true;
}
private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
{
var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS");
if (launchAsAskpass is not "TRUE")
@ -460,14 +467,16 @@ 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;
}
return false;
}
private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{
Native.OS.SetupEnternalTools();
Models.AvatarManager.Instance.Start();
@ -480,7 +489,7 @@ namespace SourceGit
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
#if !DISABLE_UPDATE_DETECTION
var pref = ViewModels.Preference.Instance;
var pref = ViewModels.Preferences.Instance;
if (pref.ShouldCheck4UpdateOnStartup())
Check4Update();
#endif
@ -512,7 +521,7 @@ namespace SourceGit
// Should not check ignored tag if this is called manually.
if (!manually)
{
var pref = ViewModels.Preference.Instance;
var pref = ViewModels.Preferences.Instance;
if (ver.TagName == pref.IgnoreUpdateTag)
return;
}
@ -522,7 +531,7 @@ namespace SourceGit
catch (Exception e)
{
if (manually)
ShowSelfUpdateResult(e);
ShowSelfUpdateResult(new Models.SelfUpdateFailed(e));
}
});
}
@ -539,6 +548,38 @@ namespace SourceGit
});
}
private string FixFontFamilyName(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var parts = input.Split(',');
var trimmed = new List<string>();
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;
}
trimmed.Add(sb.ToString());
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;

View file

@ -12,7 +12,7 @@ namespace SourceGit.Commands
Args = includeUntracked ? "add ." : "add -u .";
}
public Add(string repo, List<Models.Change> changes)
public Add(string repo, List<string> changes)
{
WorkingDirectory = repo;
Context = repo;
@ -22,7 +22,7 @@ namespace SourceGit.Commands
foreach (var c in changes)
{
builder.Append(" \"");
builder.Append(c.Path);
builder.Append(c);
builder.Append("\"");
}
Args = builder.ToString();

View file

@ -54,21 +54,14 @@
public static bool DeleteRemote(string repo, string remote, string name)
{
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
return new Push(repo, remote, $"refs/heads/{name}", true).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}";
return cmd.Exec();
}
}

View file

@ -12,7 +12,7 @@ namespace SourceGit.Commands
WorkingDirectory = path;
TraitErrorAsOutput = true;
SSHKey = sshKey;
Args = "clone --progress --verbose --recurse-submodules ";
Args = "clone --progress --verbose ";
if (!string.IsNullOrEmpty(extraArgs))
Args += $"{extraArgs} ";

View file

@ -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,7 +26,7 @@ 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;
@ -43,36 +39,15 @@ namespace SourceGit.Commands
var start = CreateGitStartInfo();
var errs = new List<string>();
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);
@ -97,9 +72,25 @@ namespace SourceGit.Commands
errs.Add(e.Data);
};
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)
{
@ -113,10 +104,18 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine();
proc.WaitForExit();
if (dummy != null)
{
lock (dummyProcLock)
{
dummy = null;
}
}
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0)
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
if (RaiseError)
{
@ -192,13 +191,12 @@ 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)

View file

@ -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,6 +20,15 @@ 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<Models.Change> Result()
{
Exec();
@ -29,7 +40,17 @@ namespace SourceGit.Commands
{
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 +69,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);

View file

@ -29,7 +29,7 @@ namespace SourceGit.Commands
var rs = new Dictionary<string, string>();
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);

View file

@ -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=dirty --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;
}

View file

@ -68,6 +68,18 @@ namespace SourceGit.Commands
return;
}
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.IsBinary)
return;

View file

@ -8,7 +8,26 @@ namespace SourceGit.Commands
{
public static class ExecuteCustomAction
{
public static void Run(string repo, string file, string args, Action<string> outputHandler)
public static void Run(string repo, string file, string args)
{
var start = new ProcessStartInfo();
start.FileName = file;
start.Arguments = args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.WorkingDirectory = repo;
try
{
Process.Start(start);
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
}
public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
{
var start = new ProcessStartInfo();
start.FileName = file;
@ -21,14 +40,6 @@ namespace SourceGit.Commands
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();
@ -57,19 +68,14 @@ namespace SourceGit.Commands
var exitCode = proc.ExitCode;
if (exitCode != 0)
{
var errMsg = builder.ToString();
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, errMsg);
});
var errMsg = builder.ToString().Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, errMsg));
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, e.Message);
});
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
proc.Close();

View file

@ -4,7 +4,7 @@ namespace SourceGit.Commands
{
public class Fetch : Command
{
public Fetch(string repo, string remote, bool noTags, bool prune, bool force, Action<string> outputHandler)
public Fetch(string repo, string remote, bool noTags, bool force, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
@ -21,9 +21,6 @@ namespace SourceGit.Commands
if (force)
Args += "--force ";
if (prune)
Args += "--prune ";
Args += remote;
}

View file

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Text;
using System.Threading;
using Avalonia.Threading;
namespace SourceGit.Commands
{
/// <summary>
@ -20,82 +22,78 @@ namespace SourceGit.Commands
}
}
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> 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<Models.Change> _changes;
private CancellationToken _cancelToken;
private Action<string> _onProgress;
private Action<string> _onResponse;
}
}

View file

@ -0,0 +1,24 @@
using System.IO;
namespace SourceGit.Commands
{
public class IsBareRepository : Command
{
public IsBareRepository(string path)
{
WorkingDirectory = path;
Args = "rev-parse --is-bare-repository";
}
public bool Result()
{
if (!Directory.Exists(Path.Combine(WorkingDirectory, "refs")) ||
!Directory.Exists(Path.Combine(WorkingDirectory, "objects")) ||
!File.Exists(Path.Combine(WorkingDirectory, "HEAD")))
return false;
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Trim() == "true";
}
}
}

View file

@ -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");
}
}
}

View file

@ -7,7 +7,7 @@ namespace SourceGit.Commands
{
public partial class LFS
{
[GeneratedRegex(@"^(.+)\s+(\w+)\s+\w+:(\d+)$")]
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK();
class SubCmd : Command
@ -82,7 +82,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);

View file

@ -13,9 +13,12 @@ namespace SourceGit.Commands
cmd.Context = repo;
cmd.RaiseError = true;
// NOTE: If no <file> 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();
}
@ -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();
}

View file

@ -4,21 +4,20 @@ namespace SourceGit.Commands
{
public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, bool prune, Action<string> outputHandler)
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
{
_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 ";
Args += "--rebase=true ";
if (noTags)
Args += "--no-tags ";
if (prune)
Args += "--prune ";
Args += $"{remote} {branch}";
}

View file

@ -26,7 +26,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,7 +36,7 @@ namespace SourceGit.Commands
if (isDelete)
Args += "--delete ";
Args += $"{remote} refs/tags/{tag}";
Args += $"{remote} {refname}";
}
protected override void OnReadline(string line)

View file

@ -24,12 +24,23 @@ namespace SourceGit.Commands
if (!rs.IsSuccess)
return branches;
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var remoteBranches = new HashSet<string>();
foreach (var line in lines)
{
var b = ParseLine(line);
if (b != null)
{
branches.Add(b);
if (!b.IsLocal)
remoteBranches.Add(b.FullName);
}
}
foreach (var b in branches)
{
if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream))
b.IsUpsteamGone = !remoteBranches.Contains(b.Upstream);
}
return branches;
@ -75,6 +86,7 @@ namespace SourceGit.Commands
branch.Head = parts[1];
branch.IsCurrent = parts[2] == "*";
branch.Upstream = parts[3];
branch.IsUpsteamGone = false;
if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();

View file

@ -9,10 +9,10 @@ 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<string> Result()
public List<string> Result()
{
Exec();
return _lines;

View file

@ -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()

View file

@ -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]
};
}
}
}

View file

@ -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,9 +18,13 @@ 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.ByCommitter)
{
search += $"-i --committer=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByFile)
{
@ -31,7 +35,7 @@ namespace SourceGit.Commands
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);
@ -44,7 +48,7 @@ namespace SourceGit.Commands
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 +124,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;

View file

@ -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<Models.CommitWithMessage> Result()
public List<Models.InteractiveCommit> 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<Models.CommitWithMessage> _commits = new List<Models.CommitWithMessage>();
private Models.CommitWithMessage _current = null;
private List<Models.InteractiveCommit> _commits = [];
private Models.InteractiveCommit _current = null;
private string _boundary = "";
}
}

View file

@ -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));
}
}
}

View file

@ -13,7 +13,7 @@ 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<Models.Change> Result()

View file

@ -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))

View file

@ -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;
}
}
}

View file

@ -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<string> 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<string>();
foreach (var line in lines)
outs.Add(line);
return outs;
}
}
}

View file

@ -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()

View file

@ -24,7 +24,7 @@ namespace SourceGit.Commands
if (rs.IsSuccess)
{
var changes = new List<Models.Change>();
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT2().Match(line);

View file

@ -1,60 +1,76 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
/// <summary>
/// Query stash changes. Requires git >= 2.32.0
/// </summary>
public partial class QueryStashChanges : Command
{
[GeneratedRegex(@"^(\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 QueryStashChanges(string repo, string sha)
public QueryStashChanges(string repo, string stash)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
Args = $"stash show -u --name-status \"{stash}\"";
}
public List<Models.Change> Result()
{
Exec();
return _changes;
}
var rs = ReadToEnd();
if (!rs.IsSuccess)
return [];
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[0])
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var outs = new List<Models.Change>();
foreach (var line in lines)
{
case 'M':
change.Set(Models.ChangeState.Modified);
_changes.Add(change);
break;
case 'A':
change.Set(Models.ChangeState.Added);
_changes.Add(change);
break;
case 'D':
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);
break;
}
}
var match = REG_FORMAT().Match(line);
if (!match.Success)
{
match = REG_RENAME_FORMAT().Match(line);
if (match.Success)
{
var renamed = new Models.Change() { Path = match.Groups[1].Value };
renamed.Set(Models.ChangeState.Renamed);
outs.Add(renamed);
}
private readonly List<Models.Change> _changes = new List<Models.Change>();
continue;
}
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0])
{
case 'M':
change.Set(Models.ChangeState.Modified);
outs.Add(change);
break;
case 'A':
change.Set(Models.ChangeState.Added);
outs.Add(change);
break;
case 'D':
change.Set(Models.ChangeState.Deleted);
outs.Add(change);
break;
case 'C':
change.Set(Models.ChangeState.Copied);
outs.Add(change);
break;
}
}
outs.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
return outs;
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
@ -8,7 +9,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s";
Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s";
}
public List<Models.Stash> Result()
@ -26,21 +27,32 @@ namespace SourceGit.Commands
_stashes.Add(_current);
break;
case 1:
_current.Time = ulong.Parse(line);
ParseParent(line);
break;
case 2:
_current.Name = line;
_current.Time = ulong.Parse(line);
break;
case 3:
_current.Name = line;
break;
case 4:
_current.Message = line;
break;
}
_nextLineIdx++;
if (_nextLineIdx > 3)
if (_nextLineIdx > 4)
_nextLineIdx = 0;
}
private void ParseParent(string data)
{
if (data.Length < 8)
return;
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
private int _nextLineIdx = 0;

View file

@ -24,11 +24,9 @@ namespace SourceGit.Commands
{
var submodules = new List<Models.Submodule>();
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'], System.StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT1().Match(line);
@ -51,13 +49,13 @@ namespace SourceGit.Commands
if (submodules.Count > 0)
{
Args = $"status -uno --porcelain -- {builder}";
Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;
var dirty = new HashSet<string>();
lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_STATUS().Match(line);

View file

@ -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] == '>')

View file

@ -37,6 +37,19 @@ namespace SourceGit.Commands
return true;
}
public static bool ProcessStashChanges(string repo, List<Models.DiffOption> 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();

View file

@ -11,16 +11,26 @@ namespace SourceGit.Commands
Context = repo;
}
public bool Push(string message)
public bool Push(string message, bool includeUntracked = true, bool keepIndex = false)
{
Args = $"stash push -m \"{message}\"";
var builder = new StringBuilder();
builder.Append("stash push ");
if (includeUntracked)
builder.Append("--include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\"");
Args = builder.ToString();
return Exec();
}
public bool Push(string message, List<Models.Change> 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 \"");
@ -37,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)
@ -63,21 +73,22 @@ namespace SourceGit.Commands
return Exec();
}
public bool Apply(string name)
public bool Apply(string name, bool restoreIndex)
{
Args = $"stash apply -q {name}";
var opts = restoreIndex ? "--index" : string.Empty;
Args = $"stash apply -q {opts} \"{name}\"";
return Exec();
}
public bool Pop(string name)
{
Args = $"stash pop -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();
}

View file

@ -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()

View file

@ -48,9 +48,7 @@ namespace SourceGit.Commands
if (remotes != null)
{
foreach (var r in remotes)
{
new Push(repo, r.Name, name, true).Exec();
}
new Push(repo, r.Name, $"refs/tags/{name}", true).Exec();
}
return true;

View file

@ -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))
@ -73,6 +75,8 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(tracking))
Args += tracking;
else if (!string.IsNullOrEmpty(name) && !createNew)
Args += name;
_outputHandler = outputHandler;
return Exec();

View file

@ -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<bool, double> ToPageTabWidth =
new FuncValueConverter<bool, double>(x => x ? 200 : double.NaN);
public static readonly FuncValueConverter<bool, FontWeight> IsBoldToFontWeight =
new FuncValueConverter<bool, FontWeight>(x => x ? FontWeight.Bold : FontWeight.Normal);
}
}

View file

@ -23,10 +23,10 @@ namespace SourceGit.Converters
new FuncValueConverter<int, bool>(v => v != 1);
public static readonly FuncValueConverter<int, bool> IsSubjectLengthBad =
new FuncValueConverter<int, bool>(v => v > ViewModels.Preference.Instance.SubjectGuideLength);
new FuncValueConverter<int, bool>(v => v > ViewModels.Preferences.Instance.SubjectGuideLength);
public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood =
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength);
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preferences.Instance.SubjectGuideLength);
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));

View file

@ -78,5 +78,11 @@ namespace SourceGit.Converters
return v.Substring(13);
return v;
});
public static readonly FuncValueConverter<string, bool> ContainsSpaces =
new FuncValueConverter<string, bool>(v => v != null && v.Contains(' '));
public static readonly FuncValueConverter<string, bool> IsNotNullOrWhitespace =
new FuncValueConverter<string, bool>(v => v != null && v.Trim().Length > 0);
}
}

View file

@ -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;
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -196,8 +196,8 @@ 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).AsSpan());
var builder = new StringBuilder(hash.Length * 2);
foreach (var c in hash)
builder.Append(c.ToString("x2"));
return builder.ToString();

View file

@ -34,6 +34,7 @@ namespace SourceGit.Models
public string Upstream { get; set; }
public BranchTrackStatus TrackStatus { get; set; }
public string Remote { get; set; }
public bool IsUpsteamGone { get; set; }
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
}

View file

@ -36,7 +36,7 @@ namespace SourceGit.Models
public string OriginalPath { get; set; } = "";
public ChangeDataForAmend DataForAmend { get; set; } = null;
public bool IsConflit
public bool IsConflict
{
get
{

View file

@ -10,7 +10,9 @@ namespace SourceGit.Models
{
public enum CommitSearchMethod
{
ByUser,
BySHA = 0,
ByAuthor,
ByCommitter,
ByMessage,
ByFile,
}
@ -43,6 +45,7 @@ namespace SourceGit.Models
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public bool IsMerged { get; set; } = false;
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
@ -149,9 +152,9 @@ namespace SourceGit.Models
}
}
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 List<Hyperlink> Links { get; set; } = [];
}
}

View file

@ -6,6 +6,7 @@ namespace SourceGit.Models
{
Repository,
Commit,
Branch,
}
public class CustomAction : ObservableObject
@ -34,9 +35,16 @@ namespace SourceGit.Models
set => SetProperty(ref _arguments, value);
}
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;
}
}

View file

@ -32,17 +32,17 @@ namespace SourceGit.Models
public static readonly List<DateTimeFormat> Supported = new List<DateTimeFormat>
{
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);

View file

@ -1,9 +0,0 @@
namespace SourceGit.Models
{
public enum DealWithLocalChanges
{
DoNothing,
StashAndReaply,
Discard,
}
}

View file

@ -681,6 +681,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}";
}
}
}
}

View file

@ -42,6 +42,7 @@ 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\""),
};
}
else if (OperatingSystem.IsMacOS())

View file

@ -13,13 +13,18 @@
public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0);
/// <summary>
/// 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 `--pathspec-from-file` option.
/// </summary>
public static readonly System.Version STASH_WITH_PATHSPECFILE = new System.Version(2, 26, 0);
public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new System.Version(2, 26, 0);
/// <summary>
/// The minimal version of Git that supports the `stash` command with the `--staged` option.
/// The minimal version of Git that supports the `stash push` command with the `--staged` option.
/// </summary>
public static readonly System.Version STASH_ONLY_STAGED = new System.Version(2, 35, 0);
public static readonly System.Version STASH_PUSH_ONLY_STAGED = new System.Version(2, 35, 0);
/// <summary>
/// The minimal version of Git that supports the `stash show` command with the `-u` option.
/// </summary>
public static readonly System.Version STASH_SHOW_WITH_UNTRACKED = new System.Version(2, 32, 0);
}
}

View file

@ -2,9 +2,6 @@
{
public interface IRepository
{
string FullPath { get; set; }
string GitDir { get; set; }
void RefreshBranches();
void RefreshWorktrees();
void RefreshTags();

View file

@ -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;

View file

@ -17,6 +17,8 @@ namespace SourceGit.Models
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)

View file

@ -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"),
];

View file

@ -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<string> 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<OpenAIChatChoice> 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<OpenAIChatMessage> 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<string> _onUpdate = null;
private StringBuilder _thinkTail = new StringBuilder();
private HashSet<string> _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,54 @@ namespace SourceGit.Models
""";
}
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
public void Chat(string prompt, string question, CancellationToken cancellation, Action<string> onUpdate)
{
var chat = new OpenAIChatRequest() { Model = Model };
chat.AddMessage("user", prompt);
chat.AddMessage("user", question);
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
if (!string.IsNullOrEmpty(ApiKey))
var server = new Uri(_server);
var key = new ApiKeyCredential(_apiKey);
var client = null as ChatClient;
if (_server.Contains("openai.azure.com/", StringComparison.Ordinal))
{
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
client.DefaultRequestHeaders.Add("api-key", ApiKey);
else
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
var azure = new AzureOpenAIClient(server, key);
client = azure.GetChatClient(_model);
}
else
{
var openai = new OpenAIClient(key, new() { Endpoint = server });
client = openai.GetChatClient(_model);
}
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
var messages = new List<ChatMessage>();
messages.Add(_model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt));
messages.Add(new UserChatMessage(question));
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 +228,7 @@ namespace SourceGit.Models
private string _server;
private string _apiKey;
private string _model;
private bool _streaming = true;
private string _analyzeDiffPrompt;
private string _generateSubjectPrompt;
}

View file

@ -8,12 +8,12 @@ namespace SourceGit.Models
{
[GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")]
private static partial Regex REG_HTTPS();
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.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 = [

View file

@ -50,18 +50,6 @@ namespace SourceGit.Models
set;
} = true;
public DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch
{
get;
set;
} = DealWithLocalChanges.DoNothing;
public bool EnablePruneOnFetch
{
get;
set;
} = false;
public bool EnableForceOnFetch
{
get;
@ -74,12 +62,6 @@ namespace SourceGit.Models
set;
} = false;
public DealWithLocalChanges DealWithLocalChangesOnPull
{
get;
set;
} = DealWithLocalChanges.DoNothing;
public bool PreferRebaseInsteadOfMerge
{
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
{
@ -188,6 +176,12 @@ namespace SourceGit.Models
set;
} = false;
public bool AutoRestoreAfterStash
{
get;
set;
} = false;
public string PreferedOpenAIService
{
get;
@ -230,6 +224,12 @@ namespace SourceGit.Models
set;
} = [];
public int PreferredMergeMode
{
get;
set;
} = 0;
public Dictionary<string, FilterMode> CollectHistoriesFilters()
{
var map = new Dictionary<string, FilterMode>();
@ -309,128 +309,81 @@ namespace SourceGit.Models
public string BuildHistoriesFilter()
{
var includedRefs = new List<string>();
var excludedBranches = new List<string>();
var excludedRemotes = new List<string>();
var excludedTags = new List<string>();
var includedBranches = new List<string>();
var includedRemotes = new List<string>();
var includedTags = new List<string>();
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.Substring(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.Substring(11)}/*");
else if (filter.Mode == FilterMode.Excluded)
excludedBranches.Add($"{filter.Pattern.Substring(11)}/*");
excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(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.Substring(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.Substring(13)}/*");
else if (filter.Mode == FilterMode.Excluded)
excludedRemotes.Add($"{filter.Pattern.Substring(13)}/*");
excludedRemotes.Add($"--exclude=\"{filter.Pattern.Substring(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 ");
}
@ -477,11 +430,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;
}

View file

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

View file

@ -1,4 +1,5 @@
using System.Reflection;
using System;
using System.Reflection;
using System.Text.Json.Serialization;
namespace SourceGit.Models
@ -32,5 +33,24 @@ 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;
}
}
}

View file

@ -42,6 +42,7 @@ namespace SourceGit.Models
new ShellOrTerminal("mac-terminal", "Terminal", ""),
new ShellOrTerminal("iterm2", "iTerm", ""),
new ShellOrTerminal("warp", "Warp", ""),
new ShellOrTerminal("ghostty", "Ghostty", "")
};
}
else
@ -56,6 +57,7 @@ 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("custom", "Custom", ""),
};
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
@ -6,6 +7,7 @@ namespace SourceGit.Models
{
public string Name { get; set; } = "";
public string SHA { get; set; } = "";
public List<string> Parents { get; set; } = [];
public ulong Time { get; set; } = 0;
public string Message { get; set; } = "";

View file

@ -18,9 +18,9 @@ namespace SourceGit.Models
ThisWeek,
}
public class StaticsticsAuthor(string name, int count)
public class StaticsticsAuthor(User user, int count)
{
public string Name { get; set; } = name;
public User User { get; set; } = user;
public int Count { get; set; } = count;
}
@ -73,7 +73,7 @@ namespace SourceGit.Models
}
}
public void AddCommit(DateTime time, string author)
public void AddCommit(DateTime time, User author)
{
Total++;
@ -126,7 +126,7 @@ namespace SourceGit.Models
}
private StaticsticsMode _mode = StaticsticsMode.All;
private Dictionary<string, int> _mapUsers = new Dictionary<string, int>();
private Dictionary<User, int> _mapUsers = new Dictionary<User, int>();
private Dictionary<DateTime, int> _mapSamples = new Dictionary<DateTime, int>();
}
@ -150,14 +150,16 @@ namespace SourceGit.Models
public void AddCommit(string author, double timestamp)
{
var user = User.FindOrAdd(author);
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()

View file

@ -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

View file

@ -21,10 +21,11 @@ namespace SourceGit.Models
{
private static readonly ExtraGrammar[] s_extraGrammars =
[
new ExtraGrammar("source.toml", ".toml", "toml.json"),
new ExtraGrammar("source.kotlin", ".kotlin", "kotlin.json"),
new ExtraGrammar("source.hx", ".hx", "haxe.json"),
new ExtraGrammar("source.hxml", ".hxml", "hxml.json"),
new ExtraGrammar("source.toml", [".toml"], "toml.json"),
new ExtraGrammar("source.kotlin", [".kotlin", ".kt", ".kts"], "kotlin.json"),
new ExtraGrammar("source.hx", [".hx"], "haxe.json"),
new ExtraGrammar("source.hxml", [".hxml"], "hxml.json"),
new ExtraGrammar("text.html.jsp", [".jsp", ".jspf", ".tag"], "jsp.json"),
];
public static string GetScope(string file, RegistryOptions reg)
@ -36,13 +37,14 @@ namespace SourceGit.Models
extension = ".xml";
else if (extension == ".command")
extension = ".sh";
else if (extension == ".kt" || extension == ".kts")
extension = ".kotlin";
foreach (var grammar in s_extraGrammars)
{
if (grammar.Extension.Equals(extension, StringComparison.OrdinalIgnoreCase))
return grammar.Scope;
foreach (var ext in grammar.Extensions)
{
if (ext.Equals(extension, StringComparison.OrdinalIgnoreCase))
return grammar.Scope;
}
}
return reg.GetScopeByExtension(extension);
@ -71,10 +73,10 @@ namespace SourceGit.Models
return reg.GetGrammar(scopeName);
}
private record ExtraGrammar(string Scope, string Extension, string File)
private record ExtraGrammar(string Scope, List<string> Extensions, string File)
{
public readonly string Scope = Scope;
public readonly string Extension = Extension;
public readonly List<string> Extensions = Extensions;
public readonly string File = File;
}
}

View file

@ -43,6 +43,11 @@ namespace SourceGit.Models
return _caches.GetOrAdd(data, key => new User(key));
}
public override string ToString()
{
return $"{Name} <{Email}>";
}
private static ConcurrentDictionary<string, User> _caches = new ConcurrentDictionary<string, User>();
private readonly int _hash;
}

View file

@ -8,12 +8,12 @@ namespace SourceGit.Models
{
public class Watcher : IDisposable
{
public Watcher(IRepository repo)
public Watcher(IRepository repo, string fullpath, string gitDir)
{
_repo = repo;
_wcWatcher = new FileSystemWatcher();
_wcWatcher.Path = _repo.FullPath;
_wcWatcher.Path = fullpath;
_wcWatcher.Filter = "*";
_wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime;
_wcWatcher.IncludeSubdirectories = true;
@ -23,15 +23,8 @@ namespace SourceGit.Models
_wcWatcher.Deleted += OnWorkingCopyChanged;
_wcWatcher.EnableRaisingEvents = true;
// If this repository is a worktree repository, just watch the main repository's gitdir.
var gitDirNormalized = _repo.GitDir.Replace("\\", "/");
var worktreeIdx = gitDirNormalized.IndexOf(".git/worktrees/", StringComparison.Ordinal);
var repoWatchDir = _repo.GitDir;
if (worktreeIdx > 0)
repoWatchDir = _repo.GitDir.Substring(0, worktreeIdx + 4);
_repoWatcher = new FileSystemWatcher();
_repoWatcher.Path = repoWatchDir;
_repoWatcher.Path = gitDir;
_repoWatcher.Filter = "*";
_repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;
_repoWatcher.IncludeSubdirectories = true;
@ -72,6 +65,11 @@ namespace SourceGit.Models
_updateBranch = DateTime.Now.ToFileTime() - 1;
}
public void MarkTagDirtyManually()
{
_updateTags = DateTime.Now.ToFileTime() - 1;
}
public void MarkWorkingCopyDirtyManually()
{
_updateWC = DateTime.Now.ToFileTime() - 1;
@ -109,6 +107,7 @@ namespace SourceGit.Models
{
_updateBranch = 0;
_updateWC = 0;
_updateSubmodules = 0;
if (_updateTags > 0)
{
@ -119,6 +118,7 @@ namespace SourceGit.Models
Task.Run(_repo.RefreshBranches);
Task.Run(_repo.RefreshCommits);
Task.Run(_repo.RefreshWorkingCopyChanges);
Task.Run(_repo.RefreshSubmodules);
Task.Run(_repo.RefreshWorktrees);
}
@ -131,20 +131,20 @@ namespace SourceGit.Models
if (_updateSubmodules > 0 && now > _updateSubmodules)
{
_updateSubmodules = 0;
_repo.RefreshSubmodules();
Task.Run(_repo.RefreshSubmodules);
}
if (_updateStashes > 0 && now > _updateStashes)
{
_updateStashes = 0;
_repo.RefreshStashes();
Task.Run(_repo.RefreshStashes);
}
if (_updateTags > 0 && now > _updateTags)
{
_updateTags = 0;
_repo.RefreshTags();
_repo.RefreshCommits();
Task.Run(_repo.RefreshTags);
Task.Run(_repo.RefreshCommits);
}
}
@ -173,12 +173,6 @@ namespace SourceGit.Models
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
{
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
lock (_lockSubmodule)
{
if (_submodules.Count > 0)
_updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
}
}
else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal))
{

View file

@ -6,6 +6,7 @@ namespace SourceGit.Models
{
public string Branch { get; set; } = string.Empty;
public string FullPath { get; set; } = string.Empty;
public string RelativePath { get; set; } = string.Empty;
public string Head { get; set; } = string.Empty;
public bool IsBare { get; set; } = false;
public bool IsDetached { get; set; } = false;
@ -21,15 +22,15 @@ namespace SourceGit.Models
get
{
if (IsDetached)
return $"(deteched HEAD at {Head.Substring(10)})";
return $"deteched HEAD at {Head.Substring(10)}";
if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
return $"({Branch.Substring(11)})";
return Branch.Substring(11);
if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
return $"({Branch.Substring(13)})";
return Branch.Substring(13);
return $"({Branch})";
return Branch;
}
}

View file

@ -65,13 +65,16 @@ namespace SourceGit.Native
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var cwd = string.IsNullOrEmpty(workdir) ? home : workdir;
var terminal = OS.ShellOrTerminal;
var startInfo = new ProcessStartInfo();
startInfo.WorkingDirectory = cwd;
startInfo.FileName = OS.ShellOrTerminal;
startInfo.FileName = terminal;
if (OS.ShellOrTerminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase))
if (terminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase))
startInfo.Arguments = $"start --cwd \"{cwd}\"";
else if (terminal.EndsWith("ptyxis", StringComparison.OrdinalIgnoreCase))
startInfo.Arguments = $"--new-window --working-directory=\"{cwd}\"";
try
{

View file

@ -18,9 +18,22 @@ namespace SourceGit.Native
DisableDefaultApplicationMenuItems = true,
});
// Fix `PATH` env on macOS.
var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(path))
path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal))
path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path;
var customPathFile = Path.Combine(OS.DataDir, "PATH");
if (File.Exists(customPathFile))
OS.CustomPathEnv = File.ReadAllText(customPathFile).Trim();
{
var env = File.ReadAllText(customPathFile).Trim();
if (!string.IsNullOrEmpty(env))
path = env;
}
Environment.SetEnvironmentVariable("PATH", path);
}
public string FindGitExecutable()
@ -44,6 +57,8 @@ namespace SourceGit.Native
return "iTerm";
case "warp":
return "Warp";
case "ghostty":
return "Ghostty";
}
return string.Empty;

View file

@ -25,15 +25,10 @@ namespace SourceGit.Native
void OpenWithDefaultEditor(string file);
}
public static string DataDir {
get;
private set;
} = string.Empty;
public static string CustomPathEnv
public static string DataDir
{
get;
set;
private set;
} = string.Empty;
public static string GitExecutable
@ -61,12 +56,14 @@ namespace SourceGit.Native
private set;
} = new Version(0, 0, 0);
public static string ShellOrTerminal {
public static string ShellOrTerminal
{
get;
set;
} = string.Empty;
public static List<Models.ExternalTool> ExternalTools {
public static List<Models.ExternalTool> ExternalTools
{
get;
set;
} = [];
@ -165,6 +162,15 @@ namespace SourceGit.Native
_backend.OpenWithDefaultEditor(file);
}
public static string GetAbsPath(string root, string sub)
{
var fullpath = Path.Combine(root, sub);
if (OperatingSystem.IsWindows())
return fullpath.Replace('/', '\\');
return fullpath;
}
private static void UpdateGitVersion()
{
if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable))

View file

@ -8,6 +8,7 @@ using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
namespace SourceGit.Native
{
@ -152,7 +153,7 @@ namespace SourceGit.Native
public void OpenBrowser(string url)
{
var info = new ProcessStartInfo("cmd", $"/c start {url}");
var info = new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\"");
info.CreateNoWindow = true;
Process.Start(info);
}
@ -214,12 +215,17 @@ namespace SourceGit.Native
private void FixWindowFrameOnWin10(Window w)
{
var platformHandle = w.TryGetPlatformHandle();
if (platformHandle == null)
return;
// Schedule the DWM frame extension to run in the next render frame
// to ensure proper timing with the window initialization sequence
Dispatcher.UIThread.InvokeAsync(() =>
{
var platformHandle = w.TryGetPlatformHandle();
if (platformHandle == null)
return;
var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 };
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 };
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
}, DispatcherPriority.Render);
}
#region EXTERNAL_EDITOR_FINDER

View file

@ -1,7 +1,9 @@
{
"information_for_contributors": [
"This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/haxe.tmLanguage",
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage"
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage",
"The original file was licensed under the MIT License",
"https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md"
],
"fileTypes": [
"hx",
@ -2485,4 +2487,4 @@
"name": "variable.other.hx"
}
}
}
}

View file

@ -1,7 +1,9 @@
{
"information_for_contributors": [
"This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/hxml.tmLanguage",
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage"
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage",
"The original file was licensed under the MIT License",
"https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md"
],
"fileTypes": [
"hxml"
@ -67,4 +69,4 @@
],
"scopeName": "source.hxml",
"uuid": "CB1B853A-C4C8-42C3-BA70-1B1605BE51C1"
}
}

View file

@ -0,0 +1,100 @@
{
"information_for_contributors": [
"This file has been copied from https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/syntaxes/jsp.tmLanguage.json",
"The original file was licensed under the MIT License",
"https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/LICENSE"
],
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Jakarta Server Pages",
"fileTypes": ["jsp", "jspf", "tag"],
"scopeName": "text.html.jsp",
"patterns": [
{ "include": "#comment" },
{ "include": "#directive" },
{ "include": "#expression" },
{ "include": "text.html.derivative" }
],
"injections": {
"L:text.html.jsp -comment -meta.tag.directive.jsp -meta.tag.scriptlet.jsp": {
"patterns": [
{ "include": "#scriptlet" }
],
"comment": "allow scriptlets anywhere except comments and nested"
},
"L:meta.attribute (string.quoted.single.html | string.quoted.double.html) -string.template.expression.jsp": {
"patterns": [
{ "include": "#expression" },
{ "include": "text.html.derivative" }
],
"comment": "allow expressions and tags within HTML attributes (not nested)"
}
},
"repository": {
"comment": {
"name": "comment.block.jsp",
"begin": "<%--",
"end": "--%>"
},
"directive": {
"name": "meta.tag.directive.jsp",
"begin": "(<)(%@)",
"end": "(%)(>)",
"beginCaptures": {
"1": { "name": "punctuation.definition.tag.jsp" },
"2": { "name": "entity.name.tag.jsp" }
},
"endCaptures": {
"1": { "name": "entity.name.tag.jsp" },
"2": { "name": "punctuation.definition.tag.jsp" }
},
"patterns": [
{
"match": "\\b(attribute|include|page|tag|taglib|variable)\\b(?!\\s*=)",
"name": "keyword.control.directive.jsp"
},
{ "include": "text.html.basic#attribute" }
]
},
"scriptlet": {
"name": "meta.tag.scriptlet.jsp",
"contentName": "meta.embedded.block.java",
"begin": "(<)(%[\\s!=])",
"end": "(%)(>)",
"beginCaptures": {
"1": { "name": "punctuation.definition.tag.jsp" },
"2": { "name": "entity.name.tag.jsp" }
},
"endCaptures": {
"1": { "name": "entity.name.tag.jsp" },
"2": { "name": "punctuation.definition.tag.jsp" }
},
"patterns": [
{
"match": "\\{(?=\\s*(%>|$))",
"comment": "consume trailing curly brackets for fragmented scriptlets"
},
{ "include": "source.java" }
]
},
"expression": {
"name": "string.template.expression.jsp",
"contentName": "meta.embedded.block.java",
"begin": "[$#]\\{",
"end": "\\}",
"beginCaptures": {
"0": { "name": "punctuation.definition.template-expression.begin.jsp" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.template-expression.end.jsp" }
},
"patterns": [
{ "include": "#escape" },
{ "include": "source.java" }
]
},
"escape": {
"match": "\\\\.",
"name": "constant.character.escape.jsp"
}
}
}

View file

@ -1,6 +1,8 @@
{
"information_for_contributors": [
"This file has been copied from https://github.com/eclipse/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/org.eclipse.buildship.kotlindsl.provider/kotlin.tmLanguage.json"
"This file has been copied from https://github.com/eclipse/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/org.eclipse.buildship.kotlindsl.provider/kotlin.tmLanguage.json",
"The original file was licensed under the Eclipse Public License, Version 1.0",
"https://github.com/eclipse-buildship/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/README.md"
],
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Kotlin",
@ -698,4 +700,4 @@
"name": "variable.language.this.kotlin"
}
}
}
}

View file

@ -3,7 +3,10 @@
"scopeName": "source.toml",
"uuid": "8b4e5008-c50d-11ea-a91b-54ee75aeeb97",
"information_for_contributors": [
"Originally was maintained by aster (galaster@foxmail.com). This notice is only kept here for the record, please don't send e-mails about bugs and other issues."
"Originally was maintained by aster (galaster@foxmail.com). This notice is only kept here for the record, please don't send e-mails about bugs and other issues.",
"This file has been copied from https://github.com/kkiyama117/coc-toml/blob/main/toml.tmLanguage.json",
"The original file was licensed under the MIT License",
"https://github.com/kkiyama117/coc-toml/blob/main/LICENSE"
],
"patterns": [
{

View file

@ -5,12 +5,16 @@
<StreamGeometry x:Key="Icons.Binary">M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z</StreamGeometry>
<StreamGeometry x:Key="Icons.Blame">M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z</StreamGeometry>
<StreamGeometry x:Key="Icons.Bookmark">M832 64H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V96c0-18-14-32-32-32zM736 596 624 502 506 596V131h230v318z</StreamGeometry>
<StreamGeometry x:Key="Icons.Bottom">M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z</StreamGeometry>
<StreamGeometry x:Key="Icons.Branch">M757 226a143 143 0 00-55 276 96 96 0 01-88 59h-191a187 187 0 00-96 27V312a143 143 0 10-96 0v399a143 143 0 10103 2 96 96 0 0188-59h191a191 191 0 00187-151 143 143 0 00-43-279zM280 130a48 48 0 110 96 48 48 0 010-96zm0 764a48 48 0 110-96 48 48 0 010 96zM757 417a48 48 0 110-96 48 48 0 010 96z</StreamGeometry>
<StreamGeometry x:Key="Icons.Branch.Add">M896 128h-64V64c0-35-29-64-64-64s-64 29-64 64v64h-64c-35 0-64 29-64 64s29 64 64 64h64v64c0 35 29 64 64 64s64-29 64-64V256h64c35 0 64-29 64-64s-29-64-64-64zm-204 307C673 481 628 512 576 512H448c-47 0-90 13-128 35V372C394 346 448 275 448 192c0-106-86-192-192-192S64 86 64 192c0 83 54 154 128 180v280c-74 26-128 97-128 180c0 106 86 192 192 192s192-86 192-192c0-67-34-125-84-159c22-20 52-33 84-33h128c122 0 223-85 249-199c-19 4-37 7-57 7c-26 0-51-5-76-13zM256 128c35 0 64 29 64 64s-29 64-64 64s-64-29-64-64s29-64 64-64zm0 768c-35 0-64-29-64-64s29-64 64-64s64 29 64 64s-29 64-64 64z</StreamGeometry>
<StreamGeometry x:Key="Icons.Check">M512 597m-1 0a1 1 0 103 0a1 1 0 10-3 0ZM810 393 732 315 448 600 293 444 214 522l156 156 78 78 362-362z</StreamGeometry>
<StreamGeometry x:Key="Icons.CheckCircled">M512 32C246 32 32 250 32 512s218 480 480 480 480-218 480-480S774 32 512 32zm269 381L496 698c-26 26-61 26-83 0L243 528c-26-26-26-61 0-83s61-26 83 0l128 128 240-240c26-26 61-26 83 0 26 19 26 54 3 80z</StreamGeometry>
<StreamGeometry x:Key="Icons.Changes">M747 467c29 0 56 4 82 12v-363c0-47-38-84-84-84H125c-47 0-84 38-84 84v707c0 47 38 84 84 84h375a287 287 0 01-43-152c0-160 129-289 289-289zm-531-250h438c19 0 34 15 34 34s-15 34-34 34H216c-19 0-34-15-34-34s15-34 34-34zm0 179h263c19 0 34 15 34 34s-15 34-34 34H216c-19 0-34-15-34-34s15-34 34-34zm131 247h-131c-19 0-34-15-34-34s15-34 34-34h131c19 0 34 15 34 34s-15 34-34 34zM747 521c-130 0-236 106-236 236S617 992 747 992s236-106 236-236S877 521 747 521zm11 386v-65h-130c-12 0-22-10-22-22s10-22 22-22h260l-130 108zm108-192H606l130-108v65h130c12 0 22 10 22 22s-10 22-22 22z</StreamGeometry>
<StreamGeometry x:Key="Icons.CherryPick">M529 511c115 0 212 79 239 185h224a62 62 0 017 123l-7 0-224 0a247 247 0 01-479 0H65a62 62 0 01-7-123l7-0h224a247 247 0 01239-185zm0 124a124 124 0 100 247 124 124 0 000-247zm0-618c32 0 58 24 61 55l0 7V206c89 11 165 45 225 103a74 74 0 0122 45l0 9v87a62 62 0 01-123 7l-0-7v-65l-6-4c-43-33-97-51-163-53l-17-0c-74 0-133 18-180 54l-6 4v65a62 62 0 01-55 61l-7 0a62 62 0 01-61-55l-0-7V362c0-20 8-39 23-53 60-58 135-92 224-103V79c0-34 28-62 62-62z</StreamGeometry>
<StreamGeometry x:Key="Icons.CircleDown">M512 926c-229 0-414-186-414-414S283 98 512 98s414 186 414 414-186 414-414 414zm0-73c189 0 341-153 341-341S701 171 512 171 171 323 171 512s153 341 341 341zm-6-192L284 439l52-52 171 171 171-171L728 439l-222 222z</StreamGeometry>
<StreamGeometry x:Key="Icons.Clear">M512 57c251 0 455 204 455 455S763 967 512 967 57 763 57 512 261 57 512 57zm181 274c-11-11-29-11-40 0L512 472 371 331c-11-11-29-11-40 0-11 11-11 29 0 40L471 512 331 653c-11 11-11 29 0 40 11 11 29 11 40 0l141-141 141 141c11 11 29 11 40 0 11-11 11-29 0-40L552 512l141-141c11-11 11-29 0-40z</StreamGeometry>
<StreamGeometry x:Key="Icons.ClearNotifications">M591 907A85 85 0 01427 875h114a299 299 0 0050 32zM725 405c130 0 235 105 235 235s-105 235-235 235-235-105-235-235 105-235 235-235zM512 64a43 43 0 0143 43v24c126 17 229 107 264 225A298 298 0 00725 341l-4 0A235 235 0 00512 213l-5 0c-125 4-224 104-228 229l-0 6v167a211 211 0 01-26 101l-4 7-14 23h211a298 298 0 0050 85l-276-0a77 77 0 01-66-39c-13-22-14-50-2-73l2-4 22-36c10-17 16-37 17-57l0-7v-167C193 287 313 153 469 131V107a43 43 0 0139-43zm345 505L654 771a149 149 0 00202-202zM725 491a149 149 0 00-131 220l202-202A149 149 0 00725 491z</StreamGeometry>
<StreamGeometry x:Key="Icons.Clean">M797 829a49 49 0 1049 49 49 49 0 00-49-49zm147-114A49 49 0 10992 764a49 49 0 00-49-49zM928 861a49 49 0 1049 49A49 49 0 00928 861zm-5-586L992 205 851 64l-71 71a67 67 0 00-94 0l235 235a67 67 0 000-94zm-853 128a32 32 0 00-32 50 1291 1291 0 0075 112L288 552c20 0 25 21 8 37l-93 86a1282 1282 0 00120 114l100-32c19-6 28 15 14 34l-40 55c26 19 53 36 82 53a89 89 0 00115-20 1391 1391 0 00256-485l-188-188s-306 224-595 198z</StreamGeometry>
<StreamGeometry x:Key="Icons.Clone">M1280 704c0 141-115 256-256 256H288C129 960 0 831 0 672c0-126 80-232 192-272A327 327 0 01192 384c0-177 143-320 320-320 119 0 222 64 277 160C820 204 857 192 896 192c106 0 192 86 192 192 0 24-5 48-13 69C1192 477 1280 580 1280 704zm-493-128H656V352c0-18-14-32-32-32h-96c-18 0-32 14-32 32v224h-131c-29 0-43 34-23 55l211 211c12 12 33 12 45 0l211-211c20-20 6-55-23-55z</StreamGeometry>
<StreamGeometry x:Key="Icons.Code">M853 102H171C133 102 102 133 102 171v683C102 891 133 922 171 922h683C891 922 922 891 922 853V171C922 133 891 102 853 102zM390 600l-48 48L205 512l137-137 48 48L301 512l88 88zM465 819l-66-18L559 205l66 18L465 819zm218-171L634 600 723 512l-88-88 48-48L819 512 683 649z</StreamGeometry>
@ -50,6 +54,7 @@
<StreamGeometry x:Key="Icons.GitFlow.Feature">M939 94v710L512 998 85 805V94h-64A21 21 0 010 73v-0C0 61 10 51 21 51h981c12 0 21 10 21 21v0c0 12-10 21-21 21h-64zm-536 588L512 624l109 58c6 3 13 4 20 3a32 32 0 0026-37l-21-122 88-87c5-5 8-11 9-18a32 32 0 00-27-37l-122-18-54-111a32 32 0 00-57 0l-54 111-122 18c-7 1-13 4-18 9a33 33 0 001 46l88 87-21 122c-1 7-0 14 3 20a32 32 0 0043 14z</StreamGeometry>
<StreamGeometry x:Key="Icons.GitFlow.Hotfix">M236 542a32 32 0 109 63l86-12a180 180 0 0022 78l-71 47a32 32 0 1035 53l75-50a176 176 0 00166 40L326 529zM512 16C238 16 16 238 16 512s222 496 496 496 496-222 496-496S786 16 512 16zm0 896c-221 0-400-179-400-400a398 398 0 0186-247l561 561A398 398 0 01512 912zm314-154L690 622a179 179 0 004-29l85 12a32 32 0 109-63l-94-13v-49l94-13a32 32 0 10-9-63l-87 12a180 180 0 00-20-62l71-47A32 32 0 10708 252l-75 50a181 181 0 00-252 10l-115-115A398 398 0 01512 112c221 0 400 179 400 400a398 398 0 01-86 247z</StreamGeometry>
<StreamGeometry x:Key="Icons.GitFlow.Release">M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z</StreamGeometry>
<StreamGeometry x:Key="Icons.Github">M525 0C235 0 0 235 0 525c0 232 150 429 359 498 26 5 36-11 36-25 0-12-1-54-1-97-146 31-176-63-176-63-23-61-58-76-58-76-48-32 3-32 3-32 53 3 81 54 81 54 47 80 123 57 153 43 4-34 18-57 33-70-116-12-239-57-239-259 0-57 21-104 54-141-5-13-23-67 5-139 0 0 44-14 144 54 42-11 87-17 131-17s90 6 131 17C756 203 801 217 801 217c29 72 10 126 5 139 34 37 54 83 54 141 0 202-123 246-240 259 19 17 36 48 36 97 0 70-1 127-1 144 0 14 10 30 36 25 209-70 359-266 359-498C1050 235 814 0 525 0z</StreamGeometry>
<StreamGeometry x:Key="Icons.GitIgnore">M590 74 859 342V876c0 38-31 68-68 68H233c-38 0-68-31-68-68V142c0-38 31-68 68-68h357zm-12 28H233a40 40 0 00-40 38L193 142v734a40 40 0 0038 40L233 916h558a40 40 0 0040-38L831 876V354L578 102zM855 371h-215c-46 0-83-36-84-82l0-2V74h28v213c0 30 24 54 54 55l2 0h215v28zM57 489m28 0 853 0q28 0 28 28l0 284q0 28-28 28l-853 0q-28 0-28-28l0-284q0-28 28-28ZM157 717c15 0 29-6 37-13v-51h-41v22h17v18c-2 2-6 3-10 3-21 0-30-13-30-34 0-21 12-34 28-34 9 0 15 4 20 9l14-17C184 610 172 603 156 603c-29 0-54 21-54 57 0 37 24 56 54 56zM245 711v-108h-34v108h34zm69 0v-86H341V603H262v22h28V711h24zM393 711v-108h-34v108h34zm66 6c15 0 29-6 37-13v-51h-41v22h17v18c-2 2-6 3-10 3-21 0-30-13-30-34 0-21 12-34 28-34 9 0 15 4 20 9l14-17C485 610 474 603 458 603c-29 0-54 21-54 57 0 37 24 56 54 56zm88-6v-36c0-13-2-28-3-40h1l10 24 25 52H603v-108h-23v36c0 13 2 28 3 40h-1l-10-24L548 603H523v108h23zM677 717c30 0 51-22 51-57 0-36-21-56-51-56-30 0-51 20-51 56 0 36 21 57 51 57zm3-23c-16 0-26-12-26-32 0-19 10-31 26-31 16 0 26 11 26 31S696 694 680 694zm93 17v-38h13l21 38H836l-25-43c12-5 19-15 19-31 0-26-20-34-44-34H745v108h27zm16-51H774v-34h15c16 0 25 4 25 16s-9 18-25 18zM922 711v-22h-43v-23h35v-22h-35V625h41V603H853v108h68z</StreamGeometry>
<StreamGeometry x:Key="Icons.Grid">M30 271l241 0 0-241-241 0 0 241zM392 271l241 0 0-241-241 0 0 241zM753 30l0 241 241 0 0-241-241 0zM30 632l241 0 0-241-241 0 0 241zM392 632l241 0 0-241-241 0 0 241zM753 632l241 0 0-241-241 0 0 241zM30 994l241 0 0-241-241 0 0 241zM392 994l241 0 0-241-241 0 0 241zM753 994l241 0 0-241-241 0 0 241z</StreamGeometry>
<StreamGeometry x:Key="Icons.Head">M0 512M1024 512M512 0M512 1024M955 323q0 23-16 39l-414 414-78 78q-16 16-39 16t-39-16l-78-78-207-207q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 169 375-375q16-16 39-16t39 16l78 78q16 16 16 39z</StreamGeometry>
@ -86,6 +91,7 @@
<StreamGeometry x:Key="Icons.Plus">m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z</StreamGeometry>
<StreamGeometry x:Key="Icons.Pull">M432 0h160c27 0 48 21 48 48v336h175c36 0 53 43 28 68L539 757c-15 15-40 15-55 0L180 452c-25-25-7-68 28-68H384V48c0-27 21-48 48-48zm592 752v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h293l98 98c40 40 105 40 145 0l98-98H976c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z</StreamGeometry>
<StreamGeometry x:Key="Icons.Push">M592 768h-160c-27 0-48-21-48-48V384h-175c-36 0-53-43-28-68L485 11c15-15 40-15 55 0l304 304c25 25 7 68-28 68H640v336c0 27-21 48-48 48zm432-16v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h272v16c0 62 50 112 112 112h160c62 0 112-50 112-112v-16h272c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z</StreamGeometry>
<StreamGeometry x:Key="Icons.Quit">M563 555c0 28-23 51-51 51-28 0-51-23-51-51L461 113c0-28 23-51 51-51s51 23 51 51L563 555 563 555zM85 535c0-153 81-287 201-362 24-15 55-8 70 16C371 214 363 245 340 260 248 318 187 419 187 535c0 180 146 325 325 325 180-0 325-146 325-325 0-119-64-223-160-280-24-14-32-46-18-70 14-24 46-32 70-18 125 74 210 211 210 367 0 236-191 427-427 427C276 963 85 772 85 535</StreamGeometry>
<StreamGeometry x:Key="Icons.Rebase">M277 85a149 149 0 00-43 292v230a32 32 0 0064 0V555h267A160 160 0 00725 395v-12a149 149 0 10-64-5v17a96 96 0 01-96 96H299V383A149 149 0 00277 85zM228 720a32 32 0 00-37-52 150 150 0 00-53 68 32 32 0 1060 23 85 85 0 0130-39zm136-52a32 32 0 00-37 52 86 86 0 0130 39 32 32 0 1060-23 149 149 0 00-53-68zM204 833a32 32 0 10-55 32 149 149 0 0063 58 32 32 0 0028-57 85 85 0 01-36-33zm202 32a32 32 0 00-55-32 85 85 0 01-36 33 32 32 0 0028 57 149 149 0 0063-58z</StreamGeometry>
<StreamGeometry x:Key="Icons.Reference">M467 556c0-0 0-1 0-1C467 555 467 556 467 556zM467 556c0-0 0-0 0-0C467 556 467 556 467 556zM467 556c-0 0-0 0-0 0C467 557 467 557 467 556zM468 549C468 532 468 541 468 549L468 549zM468 549c0 1-0 1-0 2C468 551 468 550 468 549zM468 552c-0 1-0 2-0 3C467 554 468 553 468 552zM736 549C736 532 736 541 736 549L736 549zM289 378l0 179 89 0c-1 80-89 89-89 89l45 45c0 0 129-15 134-134L467 378 289 378zM959 244l0 536c0 99-80 179-179 179L244 959c-99 0-179-80-179-179L65 244c0-99 80-179 179-179l536 0C879 65 959 145 959 244zM869 289c0-74-60-134-134-134L289 155c-74 0-134 60-134 134l0 447c0 74 60 134 134 134l447 0c74 0 134-60 134-134L869 289zM557 557l89 0c-1 80-89 89-89 89l45 45c0 0 129-15 134-134L735 378 557 378 557 557z</StreamGeometry>
<StreamGeometry x:Key="Icons.Relation">m224 154a166 166 0 00-166 166v192a166 166 0 00166 166h64v-76h-64a90 90 0 01-90-90v-192a90 90 0 0190-90h320a90 90 0 0190 90v192a90 90 0 01-90 90h-128v77h128a166 166 0 00166-167v-192a166 166 0 00-166-166h-320zm166 390a90 90 0 0190-90h128v-76h-128a166 166 0 00-166 166v192a166 166 0 00166 166h320a166 166 0 00166-166v-192a166 166 0 00-166-166h-64v77h64a90 90 0 0190 90v192a90 90 0 01-90 90h-320a90 90 0 01-90-90v-192z</StreamGeometry>
@ -115,6 +121,7 @@
<StreamGeometry x:Key="Icons.Tags">M996 452 572 28A96 96 0 00504 0H96C43 0 0 43 0 96v408a96 96 0 0028 68l424 424c37 37 98 37 136 0l408-408c37-37 37-98 0-136zM224 320c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm1028 268L844 996c-37 37-98 37-136 0l-1-1L1055 647c34-34 53-79 53-127s-19-93-53-127L663 0h97a96 96 0 0168 28l424 424c37 37 37 98 0 136z</StreamGeometry>
<StreamGeometry x:Key="Icons.Target">M765 118 629 239l-16 137-186 160 54 59 183-168 144 4 136-129 47-43-175-12L827 67zM489 404c-66 0-124 55-124 125s54 121 124 121c66 0 120-55 120-121H489l23-121c-8-4-16-4-23-4zM695 525c0 114-93 207-206 207s-206-94-206-207 93-207 206-207c16 0 27 0 43 4l43-207c-27-4-54-8-85-8-229 0-416 188-416 419s187 419 416 419c225 0 408-180 416-403v-12l-210-4z</StreamGeometry>
<StreamGeometry x:Key="Icons.Terminal">M144 112h736c18 0 32 14 32 32v736c0 18-14 32-32 32H144c-18 0-32-14-32-32V144c0-18 14-32 32-32zm112 211v72a9 9 0 003 7L386 509 259 615a9 9 0 00-3 7v72a9 9 0 0015 7L493 516a9 9 0 000-14l-222-186a9 9 0 00-15 7zM522 624a10 10 0 00-10 10v60a10 10 0 0010 10h237a10 10 0 0010-10v-60a10 10 0 00-10-10H522z</StreamGeometry>
<StreamGeometry x:Key="Icons.Top">M170 831 513 489 855 831 960 726 512 278 64 726 170 831zM512 278h448v-128h-896v128h448z</StreamGeometry>
<StreamGeometry x:Key="Icons.Track">M897 673v13c0 51-42 93-93 93h-10c-1 0-2 0-2 0H220c-23 0-42 19-42 42v13c0 23 19 42 42 42h552c14 0 26 12 26 26 0 14-12 26-26 26H220c-51 0-93-42-93-93v-13c0-51 42-93 93-93h20c1-0 2-0 2-0h562c23 0 42-19 42-42v-13c0-11-5-22-13-29-8-7-17-11-28-10H660c-14 0-26-12-26-26 0-14 12-26 26-26h144c24-1 47 7 65 24 18 17 29 42 29 67zM479 98c-112 0-203 91-203 203 0 44 14 85 38 118l132 208c15 24 50 24 66 0l133-209c23-33 37-73 37-117 0-112-91-203-203-203zm0 327c-68 0-122-55-122-122s55-122 122-122 122 55 122 122-55 122-122 122z</StreamGeometry>
<StreamGeometry x:Key="Icons.Tree">M912 800a48 48 0 1 1 0 96h-416a48 48 0 1 1 0-96h416z m-704-704A112 112 0 0 1 256 309.184V480h80a48 48 0 0 1 0 96H256v224h81.664a48 48 0 1 1 0 96H256a96 96 0 0 1-96-96V309.248A112 112 0 0 1 208 96z m704 384a48 48 0 1 1 0 96h-416a48 48 0 0 1 0-96h416z m0-320a48 48 0 1 1 0 96h-416a48 48 0 0 1 0-96h416z</StreamGeometry>
<StreamGeometry x:Key="Icons.TriangleLeft">M30 0 30 30 0 15z</StreamGeometry>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

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