Compare commits

..

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

430 changed files with 8345 additions and 18353 deletions

View file

@ -71,20 +71,20 @@ dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase # name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# private static fields should have s_ prefix # private static fields should have s_ prefix
dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion
dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style
dotnet_naming_symbols.private_static_fields.applicable_kinds = field dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.required_modifiers = static dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
@ -93,14 +93,14 @@ dotnet_naming_style.private_static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase # internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _ dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# use accessibility modifiers # use accessibility modifiers
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion

View file

@ -19,25 +19,14 @@ jobs:
os: macos-latest os: macos-latest
runtime: osx-arm64 runtime: osx-arm64
- name : Linux - name : Linux
os: ubuntu-latest os: ubuntu-20.04
runtime: linux-x64 runtime: linux-x64
container: ubuntu:20.04
- name : Linux (arm64) - name : Linux (arm64)
os: ubuntu-latest os: ubuntu-20.04
runtime: linux-arm64 runtime: linux-arm64
container: ubuntu:20.04
name: Build ${{ matrix.name }} name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
container: ${{ matrix.container || '' }}
steps: 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 - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup .NET - name: Setup .NET
@ -58,7 +47,7 @@ jobs:
if: ${{ matrix.runtime == 'linux-arm64' }} if: ${{ matrix.runtime == 'linux-arm64' }}
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y llvm gcc-aarch64-linux-gnu sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
- name: Build - name: Build
run: dotnet build -c Release run: dotnet build -c Release
- name: Publish - name: Publish

View file

@ -4,6 +4,7 @@ on:
branches: [ develop ] branches: [ develop ]
paths: paths:
- 'src/Resources/Locales/**' - 'src/Resources/Locales/**'
- 'README.md'
workflow_dispatch: workflow_dispatch:
workflow_call: workflow_call:
@ -31,8 +32,8 @@ jobs:
git config --global user.name 'github-actions[bot]' git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com' git config --global user.email 'github-actions[bot]@users.noreply.github.com'
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then
git add TRANSLATION.md src/Resources/Locales/*.axaml git add README.md TRANSLATION.md
git commit -m 'doc: Update translation status and sort locale files' git commit -m 'doc: Update translation status and missing keys'
git push git push
else else
echo "No changes to commit" echo "No changes to commit"

View file

@ -9,10 +9,10 @@ on:
jobs: jobs:
windows: windows:
name: Package Windows name: Package Windows
runs-on: windows-2019 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
runtime: [ win-x64, win-arm64 ] runtime: [win-x64, win-arm64]
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -22,7 +22,6 @@ jobs:
name: sourcegit.${{ matrix.runtime }} name: sourcegit.${{ matrix.runtime }}
path: build/SourceGit path: build/SourceGit
- name: Package - name: Package
shell: bash
env: env:
VERSION: ${{ inputs.version }} VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }} RUNTIME: ${{ matrix.runtime }}
@ -70,7 +69,6 @@ jobs:
linux: linux:
name: Package Linux name: Package Linux
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ubuntu:20.04
strategy: strategy:
matrix: matrix:
runtime: [linux-x64, linux-arm64] runtime: [linux-x64, linux-arm64]
@ -79,10 +77,9 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download package dependencies - name: Download package dependencies
run: | run: |
export DEBIAN_FRONTEND=noninteractive sudo add-apt-repository universe
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime sudo apt-get update
apt-get update sudo apt-get install desktop-file-utils rpm libfuse2
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 - name: Download build
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@ -92,7 +89,6 @@ jobs:
env: env:
VERSION: ${{ inputs.version }} VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }} RUNTIME: ${{ matrix.runtime }}
APPIMAGE_EXTRACT_AND_RUN: 1
run: | run: |
mkdir build/SourceGit mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit

2
.gitignore vendored
View file

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

View file

@ -11,15 +11,15 @@
* Supports Windows/macOS/Linux * Supports Windows/macOS/Linux
* Opensource/Free * Opensource/Free
* Fast * Fast
* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil) * Deutsch/English/Español/Français/Italiano/Português/Русский/简体中文/繁體中文
* Built-in light/dark themes * Built-in light/dark themes
* Customize theme * Customize theme
* Visual commit graph * Visual commit graph
* Supports SSH access with each remote * Supports SSH access with each remote
* GIT commands with GUI * GIT commands with GUI
* Clone/Fetch/Pull/Push... * Clone/Fetch/Pull/Push...
* Merge/Rebase/Reset/Revert/Cherry-pick... * Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
* Amend/Reword/Squash * Amend/Reword
* Interactive rebase * Interactive rebase
* Branches * Branches
* Remotes * Remotes
@ -35,14 +35,11 @@
* Revision Diffs * Revision Diffs
* Branch Diff * Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend * Image Diff - Side-By-Side/Swipe/Blend
* Git command logs
* Search commits * Search commits
* GitFlow * GitFlow
* Git LFS * Git LFS
* Bisect
* Issue Link * Issue Link
* Workspace * Workspace
* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama)) * Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING] > [!WARNING]
@ -50,13 +47,16 @@
## Translation Status ## Translation Status
You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md) [![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.21%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-99.87%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-91.80%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-%E2%88%9A-brightgreen)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-91.53%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-99.07%25-yellow)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)](TRANSLATION.md)
> [!NOTE]
> You can find the missing keys in [TRANSLATION.md](TRANSLATION.md)
## How to Use ## How to Use
**To use this tool, you need to install Git(>=2.25.1) first.** **To use this tool, you need to install Git(>=2.23.0) first.**
You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits. You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [Github Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs. This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
@ -79,7 +79,7 @@ For **Windows** users:
``` ```
> [!NOTE] > [!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. > `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 `scoop` with follow commands: * You can install the latest stable by `scoope` with follow commands:
```shell ```shell
scoop bucket add extras scoop bucket add extras
scoop install sourcegit scoop install sourcegit
@ -93,7 +93,7 @@ For **macOS** users:
brew tap ybeapps/homebrew-sourcegit brew tap ybeapps/homebrew-sourcegit
brew install --cask --no-quarantine sourcegit brew install --cask --no-quarantine sourcegit
``` ```
* If you want to install `SourceGit.app` from GitHub Release manually, you need run following command to make sure it works: * If you want to install `SourceGit.app` from Github Release manually, you need run following command to make sure it works:
```shell ```shell
sudo xattr -cr /Applications/SourceGit.app sudo xattr -cr /Applications/SourceGit.app
``` ```
@ -201,7 +201,3 @@ dotnet run --project src/SourceGit.csproj
Thanks to all the people who contribute. 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) [![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,8 +60,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control 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 EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}"

View file

@ -1,86 +0,0 @@
# 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,228 +1,33 @@
# Translation Status ### de_DE.axaml: 99.21%
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-%E2%88%9A-brightgreen)
### ![es__ES](https://img.shields.io/badge/es__ES-99.13%25-yellow)
<details> <details>
<summary>Missing keys in es_ES.axaml</summary> <summary>Missing Keys</summary>
- Text.CommitCM.PushRevision - Text.BranchUpstreamInvalid
- Text.Merge.Edit - Text.Configure.CustomAction.WaitForExit
- Text.Push.Revision - Text.Diff.First
- Text.Push.Revision.Title - Text.Diff.Last
- Text.Stash.Mode - Text.Preferences.AI.Streaming
- Text.StashCM.CopyMessage - Text.StashCM.SaveAsPatch
- Text.WorkingCopy.AddToGitIgnore.InFolder
</details> </details>
### ![fr__FR](https://img.shields.io/badge/fr__FR-91.19%25-yellow) ### es_ES.axaml: 99.87%
<details> <details>
<summary>Missing keys in fr_FR.axaml</summary> <summary>Missing Keys</summary>
- Text.Avatar.Load - Text.StashCM.SaveAsPatch
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details> </details>
### ![it__IT](https://img.shields.io/badge/it__IT-96.53%25-yellow) ### fr_FR.axaml: 91.80%
<details> <details>
<summary>Missing keys in it_IT.axaml</summary> <summary>Missing Keys</summary>
- Text.Avatar.Load
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.ClearStashes
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ResetAuthor
</details>
### ![ja__JP](https://img.shields.io/badge/ja__JP-90.94%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CompareWithCurrent
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.FilterCommits
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-83.25%25-yellow)
<details>
<summary>Missing keys in pt_BR.axaml</summary>
- Text.AIAssistant.Regen - Text.AIAssistant.Regen
- Text.AIAssistant.Use - Text.AIAssistant.Use
@ -230,88 +35,122 @@ This document shows the translation status of each locale file in the repository
- Text.ApplyStash.DropAfterApply - Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex - Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash - Text.ApplyStash.Stash
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CustomAction - Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
- Text.BranchCM.ResetToSelectedCommit
- Text.BranchUpstreamInvalid - Text.BranchUpstreamInvalid
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.Clone.RecurseSubmodules - Text.Clone.RecurseSubmodules
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.CustomAction.Scope.Branch - Text.Configure.CustomAction.Scope.Branch
- Text.Configure.CustomAction.WaitForExit - 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.CreateBranch.Name.WarnSpace
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.DeleteRepositoryNode.Path - Text.DeleteRepositoryNode.Path
- Text.DeleteRepositoryNode.TipForGroup - Text.DeleteRepositoryNode.TipForGroup
- Text.DeleteRepositoryNode.TipForRepository - Text.DeleteRepositoryNode.TipForRepository
- Text.Diff.First - Text.Diff.First
- Text.Diff.Last - Text.Diff.Last
- Text.Diff.Submodule.Deleted
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.Clone
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.InProgress.CherryPick.Head - Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating - Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt - Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head - Text.InProgress.Revert.Head
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Merge.Source - Text.Merge.Source
- Text.MergeMultiple - Text.MergeMultiple
- Text.MergeMultiple.CommitChanges - Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy - Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets - Text.MergeMultiple.Targets
- Text.Preferences.AI.Streaming - Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth - Text.Preferences.Appearance.FontSize
- Text.Preferences.Appearance.FontSize.Default
- Text.Preferences.Appearance.FontSize.Editor
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
- Text.Preferences.Git.SSLVerify
- Text.Repository.CustomActions
- Text.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.Notifications.Clear
- 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.Stash.AutoRestore
- Text.Stash.AutoRestore.Tip
- Text.StashCM.SaveAsPatch
- Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.SignOff
</details>
### it_IT.axaml: 100.00%
<details>
<summary>Missing Keys</summary>
</details>
### pt_BR.axaml: 91.53%
<details>
<summary>Missing Keys</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.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.IssueTracker.AddSampleGiteeIssue
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
- 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
- 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.Preferences.AI.Streaming
- Text.Preferences.General.DateFormat - Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren - Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Preferences.Git.SSLVerify - Text.Preferences.Git.SSLVerify
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.FilterCommits - Text.Repository.FilterCommits
- Text.Repository.HistoriesLayout - Text.Repository.HistoriesLayout
- Text.Repository.HistoriesLayout.Horizontal - Text.Repository.HistoriesLayout.Horizontal
@ -319,201 +158,57 @@ This document shows the translation status of each locale file in the repository
- Text.Repository.HistoriesOrder - Text.Repository.HistoriesOrder
- Text.Repository.Notifications.Clear - Text.Repository.Notifications.Clear
- Text.Repository.OnlyHighlightCurrentBranchInHistories - Text.Repository.OnlyHighlightCurrentBranchInHistories
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.Skip - Text.Repository.Skip
- Text.Repository.Tags.OrderByCreatorDate - Text.Repository.Tags.OrderByCreatorDate
- Text.Repository.Tags.OrderByName - Text.Repository.Tags.OrderByNameAsc
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.Tags.Sort - Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories - Text.Repository.UseRelativeTimeInHistories
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.SetUpstream - Text.SetUpstream
- Text.SetUpstream.Local - Text.SetUpstream.Local
- Text.SetUpstream.Unset - Text.SetUpstream.Unset
- Text.SetUpstream.Upstream - Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo - Text.SHALinkCM.NavigateTo
- Text.Stash.Mode - Text.Stash.AutoRestore
- Text.StashCM.CopyMessage - Text.Stash.AutoRestore.Tip
- Text.StashCM.SaveAsPatch - Text.StashCM.SaveAsPatch
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.CommitToEdit - Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
- Text.WorkingCopy.SignOff - Text.WorkingCopy.SignOff
</details> </details>
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ru_RU.axaml: 99.07%
### ![ta__IN](https://img.shields.io/badge/ta__IN-91.07%25-yellow)
<details> <details>
<summary>Missing keys in ta_IN.axaml</summary> <summary>Missing Keys</summary>
- Text.Avatar.Load - Text.BranchCM.CustomAction
- Text.Bisect - Text.BranchUpstreamInvalid
- Text.Bisect.Abort - Text.Configure.CustomAction.Scope.Branch
- Text.Bisect.Bad - Text.Diff.First
- Text.Bisect.Detecting - Text.Diff.Last
- Text.Bisect.Good - Text.Preferences.AI.Streaming
- Text.Bisect.Skip - Text.StashCM.SaveAsPatch
- Text.Bisect.WaitingForRange
- Text.BranchCM.CompareWithCurrent
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.UpdateSubmodules.Target
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details> </details>
### ![uk__UA](https://img.shields.io/badge/uk__UA-92.31%25-yellow) ### zh_CN.axaml: 100.00%
<details> <details>
<summary>Missing keys in uk_UA.axaml</summary> <summary>Missing Keys</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.ConfigureWorkspace.Name
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ResetAuthor
</details> </details>
### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) ### zh_TW.axaml: 100.00%
### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)
<details>
<summary>Missing Keys</summary>
</details>

View file

@ -1 +1 @@
2025.23 2025.07

View file

@ -12,4 +12,4 @@
dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj
``` ```
> [!NOTE] > [!NOTE]
> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replace the `$DESTINATION_FOLDER` with the real path that will store the output executable files. > Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replece the `$DESTINATION_FOLDER` with the real path that will store the output executable files.

View file

@ -1,8 +1,7 @@
Package: sourcegit Package: sourcegit
Version: 2025.10 Version: 8.23
Priority: optional 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 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 Architecture: amd64
Installed-Size: 60440
Maintainer: longshuang@msn.cn Maintainer: longshuang@msn.cn
Description: Open-source & Free Git GUI Client Description: Open-source & Free Git GUI Client

View file

@ -1,32 +0,0 @@
#!/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

@ -1,35 +0,0 @@
#!/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,6 +6,7 @@ const repoRoot = path.join(__dirname, '../../');
const localesDir = path.join(repoRoot, 'src/Resources/Locales'); const localesDir = path.join(repoRoot, 'src/Resources/Locales');
const enUSFile = path.join(localesDir, 'en_US.axaml'); const enUSFile = path.join(localesDir, 'en_US.axaml');
const outputFile = path.join(repoRoot, 'TRANSLATION.md'); const outputFile = path.join(repoRoot, 'TRANSLATION.md');
const readmeFile = path.join(repoRoot, 'README.md');
const parser = new xml2js.Parser(); const parser = new xml2js.Parser();
@ -14,70 +15,49 @@ async function parseXml(filePath) {
return parser.parseStringPromise(data); return parser.parseStringPromise(data);
} }
async function filterAndSortTranslations(localeData, enUSKeys, enUSData) {
const strings = localeData.ResourceDictionary['x:String'];
// Remove keys that don't exist in English file
const filtered = strings.filter(item => enUSKeys.has(item.$['x:Key']));
// Sort based on the key order in English file
const enUSKeysArray = enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']);
filtered.sort((a, b) => {
const aIndex = enUSKeysArray.indexOf(a.$['x:Key']);
const bIndex = enUSKeysArray.indexOf(b.$['x:Key']);
return aIndex - bIndex;
});
return filtered;
}
async function calculateTranslationRate() { async function calculateTranslationRate() {
const enUSData = await parseXml(enUSFile); const enUSData = await parseXml(enUSFile);
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key'])); 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')); const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml'));
const lines = []; // Add en_US badge first
badges.push(`[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md)`);
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) { for (const file of files) {
const locale = file.replace('.axaml', '').replace('_', '__');
const filePath = path.join(localesDir, file); const filePath = path.join(localesDir, file);
const localeData = await parseXml(filePath); const localeData = await parseXml(filePath);
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key'])); const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key)); const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
const translationRate = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
// Sort and clean up extra translations translationRates.push(`### ${file}: ${translationRate.toFixed(2)}%\n`);
const sortedAndCleaned = await filterAndSortTranslations(localeData, enUSKeys, enUSData); translationRates.push(`<details>\n<summary>Missing Keys</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`);
localeData.ResourceDictionary['x:String'] = sortedAndCleaned;
// Save the updated file // Add badges
const builder = new xml2js.Builder({ const locale = file.replace('.axaml', '').replace('_', '__');
headless: true, if (translationRate === 100) {
renderOpts: { pretty: true, indent: ' ' } badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)](TRANSLATION.md)`);
});
let xmlStr = builder.buildObject(localeData);
// Add an empty line before the first x:String
xmlStr = xmlStr.replace(' <x:String', '\n <x:String');
await fs.writeFile(filePath, xmlStr + '\n', 'utf8');
if (missingKeys.length > 0) {
const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
const badgeColor = progress >= 75 ? 'yellow' : 'red';
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 { } else {
lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)`); const badgeColor = translationRate >= 75 ? 'yellow' : 'red';
} badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-${translationRate.toFixed(2)}%25-${badgeColor})](TRANSLATION.md)`);
}
} }
const content = lines.join('\n\n'); console.log(translationRates.join('\n\n'));
console.log(content);
await fs.writeFile(outputFile, content, 'utf8'); 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');
} }
calculateTranslationRate().catch(err => console.error(err)); calculateTranslationRate().catch(err => console.error(err));

View file

@ -56,15 +56,8 @@ cp -f SourceGit/* resources/deb/opt/sourcegit
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
cp -r resources/_common/applications resources/deb/usr/share cp -r resources/_common/applications resources/deb/usr/share
cp -r resources/_common/icons resources/deb/usr/share cp -r resources/_common/icons resources/deb/usr/share
# Calculate installed size in KB sed -i -e "s/^Version:.*/Version: $VERSION/" -e "s/^Architecture:.*/Architecture: $arch/" resources/deb/DEBIAN/control
installed_size=$(du -sk resources/deb | cut -f1) dpkg-deb --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
# 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" 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" ./ mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./

View file

@ -9,8 +9,4 @@ cd build
rm -rf SourceGit/*.pdb rm -rf SourceGit/*.pdb
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
powershell -Command "Compress-Archive -Path SourceGit -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force"
else
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
fi

View file

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

View file

@ -16,13 +16,10 @@
<ResourceInclude x:Key="fr_FR" Source="/Resources/Locales/fr_FR.axaml"/> <ResourceInclude x:Key="fr_FR" Source="/Resources/Locales/fr_FR.axaml"/>
<ResourceInclude x:Key="it_IT" Source="/Resources/Locales/it_IT.axaml"/> <ResourceInclude x:Key="it_IT" Source="/Resources/Locales/it_IT.axaml"/>
<ResourceInclude x:Key="pt_BR" Source="/Resources/Locales/pt_BR.axaml"/> <ResourceInclude x:Key="pt_BR" Source="/Resources/Locales/pt_BR.axaml"/>
<ResourceInclude x:Key="uk_UA" Source="/Resources/Locales/uk_UA.axaml"/>
<ResourceInclude x:Key="ru_RU" Source="/Resources/Locales/ru_RU.axaml"/> <ResourceInclude x:Key="ru_RU" Source="/Resources/Locales/ru_RU.axaml"/>
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/> <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="zh_TW" Source="/Resources/Locales/zh_TW.axaml"/>
<ResourceInclude x:Key="es_ES" Source="/Resources/Locales/es_ES.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> </ResourceDictionary>
</Application.Resources> </Application.Resources>
@ -35,7 +32,7 @@
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/> <NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" Gesture="F1"/> <NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/> <NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/> <NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/> <NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>

View file

@ -6,7 +6,6 @@ using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -38,6 +37,7 @@ namespace SourceGit
TaskScheduler.UnobservedTaskException += (_, e) => TaskScheduler.UnobservedTaskException += (_, e) =>
{ {
LogException(e.Exception);
e.SetObserved(); e.SetObserved();
}; };
@ -77,72 +77,13 @@ namespace SourceGit
Native.OS.SetupApp(builder); Native.OS.SetupApp(builder);
return builder; return builder;
} }
public static void LogException(Exception ex)
{
if (ex == null)
return;
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
builder.Append($"User: {Environment.UserName}\n");
builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
builder.Append($"Exception Time: {DateTime.Now}\n");
builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex);
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
File.WriteAllText(file, builder.ToString());
}
#endregion #endregion
#region Utility Functions #region Utility Functions
public static void ShowWindow(object data, bool showAsDialog) public static void OpenDialog(Window window)
{ {
var impl = (Views.ChromelessWindow target, bool isDialog) => if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{ window.ShowDialog(owner);
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
if (isDialog)
target.ShowDialog(owner);
else
target.Show(owner);
}
else
{
target.Show();
}
};
if (data is Views.ChromelessWindow window)
{
impl(window, showAsDialog);
return;
}
var dataTypeName = data.GetType().FullName;
if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal))
return;
var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
var viewType = Type.GetType(viewTypeName);
if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow)))
return;
window = Activator.CreateInstance(viewType) as Views.ChromelessWindow;
if (window != null)
{
window.DataContext = data;
impl(window, showAsDialog);
}
} }
public static void RaiseException(string context, string message) public static void RaiseException(string context, string message)
@ -240,9 +181,6 @@ namespace SourceGit
app._fontsOverrides = null; app._fontsOverrides = null;
} }
defaultFont = app.FixFontFamilyName(defaultFont);
monospaceFont = app.FixFontFamilyName(monospaceFont);
var resDic = new ResourceDictionary(); var resDic = new ResourceDictionary();
if (!string.IsNullOrEmpty(defaultFont)) if (!string.IsNullOrEmpty(defaultFont))
resDic.Add("Fonts.Default", new FontFamily(defaultFont)); resDic.Add("Fonts.Default", new FontFamily(defaultFont));
@ -301,7 +239,7 @@ namespace SourceGit
return await clipboard.GetTextAsync(); return await clipboard.GetTextAsync();
} }
} }
return null; return default;
} }
public static string Text(string key, params object[] args) public static string Text(string key, params object[] args)
@ -323,7 +261,8 @@ namespace SourceGit
icon.Height = 12; icon.Height = 12;
icon.Stretch = Stretch.Uniform; icon.Stretch = Stretch.Uniform;
if (Current?.FindResource(key) is StreamGeometry geo) var geo = Current?.FindResource(key) as StreamGeometry;
if (geo != null)
icon.Data = geo; icon.Data = geo;
return icon; return icon;
@ -337,7 +276,7 @@ namespace SourceGit
return null; return null;
} }
public static ViewModels.Launcher GetLauncher() public static ViewModels.Launcher GetLauncer()
{ {
return Current is App app ? app._launcher : null; return Current is App app ? app._launcher : null;
} }
@ -375,46 +314,42 @@ namespace SourceGit
{ {
BindingPlugins.DataValidators.RemoveAt(0); BindingPlugins.DataValidators.RemoveAt(0);
// Disable tooltip if window is not active.
ToolTip.ToolTipOpeningEvent.AddClassHandler<Control>((c, e) =>
{
var topLevel = TopLevel.GetTopLevel(c);
if (topLevel is not Window { IsActive: true })
e.Cancel = true;
});
if (TryLaunchAsCoreEditor(desktop)) if (TryLaunchAsCoreEditor(desktop))
return; return;
if (TryLaunchAsAskpass(desktop)) if (TryLaunchAsAskpass(desktop))
return; return;
_ipcChannel = new Models.IpcChannel(); TryLaunchAsNormal(desktop);
if (!_ipcChannel.IsFirstInstance)
{
var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty;
if (!string.IsNullOrEmpty(arg))
{
if (arg.StartsWith('"') && arg.EndsWith('"'))
arg = arg.Substring(1, arg.Length - 2).Trim();
if (arg.Length > 0 && !Path.IsPathFullyQualified(arg))
arg = Path.GetFullPath(arg);
}
_ipcChannel.SendToFirstInstance(arg);
Environment.Exit(0);
}
else
{
_ipcChannel.MessageReceived += TryOpenRepository;
desktop.Exit += (_, _) => _ipcChannel.Dispose();
TryLaunchAsNormal(desktop);
}
} }
} }
#endregion #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}\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());
}
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
{ {
exitCode = -1; exitCode = -1;
@ -483,37 +418,21 @@ namespace SourceGit
return true; return true;
var gitDir = Path.GetDirectoryName(file)!; var gitDir = Path.GetDirectoryName(file)!;
var origHeadFile = Path.Combine(gitDir, "rebase-merge", "orig-head");
var ontoFile = Path.Combine(gitDir, "rebase-merge", "onto");
var doneFile = Path.Combine(gitDir, "rebase-merge", "done");
var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json"); var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json");
if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile)) if (!File.Exists(jobsFile))
return true; return true;
var origHead = File.ReadAllText(origHeadFile).Trim();
var onto = File.ReadAllText(ontoFile).Trim();
var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection);
if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) var doneFile = Path.Combine(gitDir, "rebase-merge", "done");
if (!File.Exists(doneFile))
return true; return true;
var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
if (done.Length == 0) if (done.Length > collection.Jobs.Count)
return true; return true;
var current = done[^1].Trim(); var job = collection.Jobs[done.Length - 1];
var match = REG_REBASE_TODO().Match(current); File.WriteAllText(file, job.Message);
if (!match.Success)
return true;
var sha = match.Groups[1].Value;
foreach (var job in collection.Jobs)
{
if (job.SHA.StartsWith(sha))
{
File.WriteAllText(file, job.Message);
break;
}
}
return true; return true;
} }
@ -521,19 +440,15 @@ namespace SourceGit
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{ {
var args = desktop.Args; var args = desktop.Args;
if (args is not { Length: > 1 } || !args[0].Equals("--core-editor", StringComparison.Ordinal)) if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
return false; return false;
var file = args[1]; var file = args[1];
if (!File.Exists(file)) if (!File.Exists(file))
{
desktop.Shutdown(-1); desktop.Shutdown(-1);
return true; else
} desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
var editor = new Views.CommitMessageEditor();
editor.AsStandalone(file);
desktop.MainWindow = editor;
return true; return true;
} }
@ -546,9 +461,7 @@ namespace SourceGit
var args = desktop.Args; var args = desktop.Args;
if (args?.Length > 0) if (args?.Length > 0)
{ {
var askpass = new Views.Askpass(); desktop.MainWindow = new Views.Askpass(args[0]);
askpass.TxtDescription.Text = args[0];
desktop.MainWindow = askpass;
return true; return true;
} }
@ -557,54 +470,23 @@ namespace SourceGit
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{ {
Native.OS.SetupExternalTools(); Native.OS.SetupEnternalTools();
Models.AvatarManager.Instance.Start(); Models.AvatarManager.Instance.Start();
string startupRepo = null; string startupRepo = null;
if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]))
startupRepo = desktop.Args[0]; startupRepo = desktop.Args[0];
var pref = ViewModels.Preferences.Instance;
pref.SetCanModify();
_launcher = new ViewModels.Launcher(startupRepo); _launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
#if !DISABLE_UPDATE_DETECTION #if !DISABLE_UPDATE_DETECTION
var pref = ViewModels.Preferences.Instance;
if (pref.ShouldCheck4UpdateOnStartup()) if (pref.ShouldCheck4UpdateOnStartup())
Check4Update(); Check4Update();
#endif #endif
} }
private void TryOpenRepository(string repo)
{
if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo))
{
var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd();
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
{
Dispatcher.UIThread.Invoke(() =>
{
var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false);
ViewModels.Welcome.Instance.Refresh();
_launcher?.OpenRepositoryInTab(node, null);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd })
wnd.BringToTop();
});
return;
}
}
Dispatcher.UIThread.Invoke(() =>
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher })
launcher.BringToTop();
});
}
private void Check4Update(bool manually = false) private void Check4Update(bool manually = false)
{ {
Task.Run(async () => Task.Run(async () =>
@ -641,7 +523,7 @@ namespace SourceGit
catch (Exception e) catch (Exception e)
{ {
if (manually) if (manually)
ShowSelfUpdateResult(new Models.SelfUpdateFailed(e)); ShowSelfUpdateResult(e);
} }
}); });
} }
@ -650,54 +532,14 @@ namespace SourceGit
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } };
dialog.ShowDialog(owner);
}
}); });
} }
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;
}
var name = sb.ToString();
if (name.Contains('#', StringComparison.Ordinal))
{
if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) &&
!name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal))
continue;
}
trimmed.Add(name);
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
[GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")]
private static partial Regex REG_REBASE_TODO();
private Models.IpcChannel _ipcChannel = null;
private ViewModels.Launcher _launcher = null; private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null; private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null; private ResourceDictionary _themeOverrides = null;

View file

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

View file

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

View file

@ -1,12 +1,23 @@
namespace SourceGit.Commands using System;
namespace SourceGit.Commands
{ {
public class Archive : Command public class Archive : Command
{ {
public Archive(string repo, string revision, string saveTo) public Archive(string repo, string revision, string saveTo, Action<string> outputHandler)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}"; Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}";
TraitErrorAsOutput = true;
_outputHandler = outputHandler;
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
} }
} }

View file

@ -1,14 +1,75 @@
namespace SourceGit.Commands using System.Collections.Generic;
{ using System.Text.RegularExpressions;
public class AssumeUnchanged : Command
{
public AssumeUnchanged(string repo, string file, bool bAdd)
{
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
WorkingDirectory = repo; namespace SourceGit.Commands
Context = repo; {
Args = $"update-index {mode} -- \"{file}\""; public partial class AssumeUnchanged
{
[GeneratedRegex(@"^(\w)\s+(.+)$")]
private static partial Regex REG_PARSE();
class ViewCommand : Command
{
public ViewCommand(string repo)
{
WorkingDirectory = repo;
Args = "ls-files -v";
RaiseError = false;
}
public List<string> Result()
{
Exec();
return _outs;
}
protected override void OnReadline(string line)
{
var match = REG_PARSE().Match(line);
if (!match.Success)
return;
if (match.Groups[1].Value == "h")
{
_outs.Add(match.Groups[2].Value);
}
}
private readonly List<string> _outs = new List<string>();
} }
class ModCommand : Command
{
public ModCommand(string repo, string file, bool bAdd)
{
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
WorkingDirectory = repo;
Context = repo;
Args = $"update-index {mode} -- \"{file}\"";
}
}
public AssumeUnchanged(string repo)
{
_repo = repo;
}
public List<string> View()
{
return new ViewCommand(_repo).Result();
}
public void Add(string file)
{
new ModCommand(_repo, file, true).Exec();
}
public void Remove(string file)
{
new ModCommand(_repo, file, false).Exec();
}
private readonly string _repo;
} }
} }

View file

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

View file

@ -21,17 +21,10 @@ namespace SourceGit.Commands
public Models.BlameData Result() public Models.BlameData Result()
{ {
var rs = ReadToEnd(); var succ = Exec();
if (!rs.IsSuccess) if (!succ)
return _result;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{ {
ParseLine(line); return new Models.BlameData();
if (_result.IsBinary)
break;
} }
if (_needUnifyCommitSHA) if (_needUnifyCommitSHA)
@ -49,9 +42,14 @@ namespace SourceGit.Commands
return _result; return _result;
} }
private void ParseLine(string line) protected override void OnReadline(string line)
{ {
if (line.Contains('\0', StringComparison.Ordinal)) if (_result.IsBinary)
return;
if (string.IsNullOrEmpty(line))
return;
if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
{ {
_result.IsBinary = true; _result.IsBinary = true;
_result.LineInfos.Clear(); _result.LineInfos.Clear();
@ -89,7 +87,7 @@ namespace SourceGit.Commands
private readonly Models.BlameData _result = new Models.BlameData(); private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder(); private readonly StringBuilder _content = new StringBuilder();
private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly; private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
private string _lastSHA = string.Empty; private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false; private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64; private int _minSHALen = 64;

View file

@ -1,6 +1,4 @@
using System.Text; namespace SourceGit.Commands
namespace SourceGit.Commands
{ {
public static class Branch public static class Branch
{ {
@ -13,40 +11,29 @@ namespace SourceGit.Commands
return cmd.ReadToEnd().StdOut.Trim(); return cmd.ReadToEnd().StdOut.Trim();
} }
public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log) public static bool Create(string repo, string name, string basedOn)
{ {
var builder = new StringBuilder();
builder.Append("branch ");
if (force)
builder.Append("-f ");
builder.Append(name);
builder.Append(" ");
builder.Append(basedOn);
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = builder.ToString(); cmd.Args = $"branch {name} {basedOn}";
cmd.Log = log;
return cmd.Exec(); return cmd.Exec();
} }
public static bool Rename(string repo, string name, string to, Models.ICommandLog log) public static bool Rename(string repo, string name, string to)
{ {
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"branch -M {name} {to}"; cmd.Args = $"branch -M {name} {to}";
cmd.Log = log;
return cmd.Exec(); return cmd.Exec();
} }
public static bool SetUpstream(string repo, string name, string upstream, Models.ICommandLog log) public static bool SetUpstream(string repo, string name, string upstream)
{ {
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Log = log;
if (string.IsNullOrEmpty(upstream)) if (string.IsNullOrEmpty(upstream))
cmd.Args = $"branch {name} --unset-upstream"; cmd.Args = $"branch {name} --unset-upstream";
@ -56,27 +43,32 @@ namespace SourceGit.Commands
return cmd.Exec(); return cmd.Exec();
} }
public static bool DeleteLocal(string repo, string name, Models.ICommandLog log) public static bool DeleteLocal(string repo, string name)
{ {
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"branch -D {name}"; cmd.Args = $"branch -D {name}";
cmd.Log = log;
return cmd.Exec(); return cmd.Exec();
} }
public static bool DeleteRemote(string repo, string remote, string name, Models.ICommandLog log) 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) { Log = log }.Exec();
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"branch -D -r {remote}/{name}";
cmd.Log = log; 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}";
}
return cmd.Exec(); return cmd.Exec();
} }
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Commands namespace SourceGit.Commands
@ -11,37 +12,19 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
} }
public bool Branch(string branch, bool force) public bool Branch(string branch, Action<string> onProgress)
{ {
var builder = new StringBuilder(); Args = $"checkout --recurse-submodules --progress {branch}";
builder.Append("checkout --progress "); TraitErrorAsOutput = true;
if (force) _outputHandler = onProgress;
builder.Append("--force ");
builder.Append(branch);
Args = builder.ToString();
return Exec(); return Exec();
} }
public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite) public bool Branch(string branch, string basedOn, Action<string> onProgress)
{ {
var builder = new StringBuilder(); Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}";
builder.Append("checkout --progress "); TraitErrorAsOutput = true;
if (force) _outputHandler = onProgress;
builder.Append("--force ");
builder.Append(allowOverwrite ? "-B " : "-b ");
builder.Append(branch);
builder.Append(" ");
builder.Append(basedOn);
Args = builder.ToString();
return Exec();
}
public bool Commit(string commitId, bool force)
{
var option = force ? "--force" : string.Empty;
Args = $"checkout {option} --detach --progress {commitId}";
return Exec(); return Exec();
} }
@ -78,5 +61,20 @@ namespace SourceGit.Commands
Args = $"checkout --no-overlay {revision} -- \"{file}\""; Args = $"checkout --no-overlay {revision} -- \"{file}\"";
return Exec(); return Exec();
} }
public bool Commit(string commitId, Action<string> onProgress)
{
Args = $"checkout --detach --progress {commitId}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
} }
} }

View file

@ -1,12 +1,31 @@
namespace SourceGit.Commands using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{ {
public class Clean : Command public class Clean : Command
{ {
public Clean(string repo) public Clean(string repo, bool includeIgnored)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "clean -qfdx"; Args = includeIgnored ? "clean -qfdx" : "clean -qfd";
}
public Clean(string repo, List<string> files)
{
var builder = new StringBuilder();
builder.Append("clean -qfd --");
foreach (var f in files)
{
builder.Append(" \"");
builder.Append(f);
builder.Append("\"");
}
WorkingDirectory = repo;
Context = repo;
Args = builder.ToString();
} }
} }
} }

View file

@ -1,11 +1,16 @@
namespace SourceGit.Commands using System;
namespace SourceGit.Commands
{ {
public class Clone : Command public class Clone : Command
{ {
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs) private readonly Action<string> _notifyProgress;
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler)
{ {
Context = ctx; Context = ctx;
WorkingDirectory = path; WorkingDirectory = path;
TraitErrorAsOutput = true;
SSHKey = sshKey; SSHKey = sshKey;
Args = "clone --progress --verbose "; Args = "clone --progress --verbose ";
@ -16,6 +21,13 @@
if (!string.IsNullOrEmpty(localName)) if (!string.IsNullOrEmpty(localName))
Args += localName; Args += localName;
_notifyProgress = ouputHandler;
}
protected override void OnReadline(string line)
{
_notifyProgress?.Invoke(line);
} }
} }
} }

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using Avalonia.Threading; using Avalonia.Threading;
@ -11,6 +10,11 @@ namespace SourceGit.Commands
{ {
public partial class Command public partial class Command
{ {
public class CancelToken
{
public bool Requested { get; set; } = false;
}
public class ReadToEndResult public class ReadToEndResult
{ {
public bool IsSuccess { get; set; } = false; public bool IsSuccess { get; set; } = false;
@ -26,51 +30,82 @@ namespace SourceGit.Commands
} }
public string Context { get; set; } = string.Empty; public string Context { get; set; } = string.Empty;
public CancellationToken CancellationToken { get; set; } = CancellationToken.None; public CancelToken Cancel { get; set; } = null;
public string WorkingDirectory { get; set; } = null; public string WorkingDirectory { get; set; } = null;
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
public string SSHKey { get; set; } = string.Empty; public string SSHKey { get; set; } = string.Empty;
public string Args { get; set; } = string.Empty; public string Args { get; set; } = string.Empty;
public bool RaiseError { get; set; } = true; public bool RaiseError { get; set; } = true;
public Models.ICommandLog Log { get; set; } = null; public bool TraitErrorAsOutput { get; set; } = false;
public bool Exec() public bool Exec()
{ {
Log?.AppendLine($"$ git {Args}\n");
var start = CreateGitStartInfo(); var start = CreateGitStartInfo();
var errs = new List<string>(); var errs = new List<string>();
var proc = new Process() { StartInfo = start }; var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); proc.OutputDataReceived += (_, e) =>
proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs); {
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (string.IsNullOrEmpty(e.Data))
{
errs.Add(string.Empty);
return;
}
if (TraitErrorAsOutput)
OnReadline(e.Data);
// Ignore progress messages
if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal))
return;
if (REG_PROGRESS().IsMatch(e.Data))
return;
errs.Add(e.Data);
};
var dummy = null as Process;
var dummyProcLock = new object();
try try
{ {
proc.Start(); 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) catch (Exception e)
{ {
if (RaiseError) if (RaiseError)
Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message)); Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message));
Log?.AppendLine(string.Empty);
return false; return false;
} }
@ -78,19 +113,10 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine(); proc.BeginErrorReadLine();
proc.WaitForExit(); proc.WaitForExit();
if (dummy != null)
{
lock (dummyProcLock)
{
dummy = null;
}
}
int exitCode = proc.ExitCode; int exitCode = proc.ExitCode;
proc.Close(); proc.Close();
Log?.AppendLine(string.Empty);
if (!CancellationToken.IsCancellationRequested && exitCode != 0) if (!isCancelled && exitCode != 0)
{ {
if (RaiseError) if (RaiseError)
{ {
@ -137,6 +163,11 @@ namespace SourceGit.Commands
return rs; return rs;
} }
protected virtual void OnReadline(string line)
{
// Implemented by derived class
}
private ProcessStartInfo CreateGitStartInfo() private ProcessStartInfo CreateGitStartInfo()
{ {
var start = new ProcessStartInfo(); var start = new ProcessStartInfo();
@ -161,12 +192,13 @@ namespace SourceGit.Commands
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey)) if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'"); start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
// Force using en_US.UTF-8 locale // Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux()) if (OperatingSystem.IsLinux())
{ start.Environment.Add("LANG", "en_US.UTF-8");
start.Environment.Add("LANG", "C");
start.Environment.Add("LC_ALL", "C"); // Fix macOS `PATH` env
} if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
// Force using this app as git editor. // Force using this app as git editor.
switch (Editor) switch (Editor)
@ -192,28 +224,6 @@ namespace SourceGit.Commands
return start; return start;
} }
private void HandleOutput(string line, List<string> errs)
{
line ??= string.Empty;
Log?.AppendLine(line);
// Lines to hide in error message.
if (line.Length > 0)
{
if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) ||
line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) ||
line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) ||
line.StartsWith("Filtering content:", StringComparison.Ordinal) ||
line.StartsWith("hint:", StringComparison.Ordinal))
return;
if (REG_PROGRESS().IsMatch(line))
return;
}
errs.Add(line);
}
[GeneratedRegex(@"\d+%")] [GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS(); private static partial Regex REG_PROGRESS();
} }

View file

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

View file

@ -6,10 +6,8 @@ namespace SourceGit.Commands
{ {
public partial class CompareRevisions : Command public partial class CompareRevisions : Command
{ {
[GeneratedRegex(@"^([MADC])\s+(.+)$")] [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT(); 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) public CompareRevisions(string repo, string start, string end)
{ {
@ -31,33 +29,16 @@ namespace SourceGit.Commands
public List<Models.Change> Result() public List<Models.Change> Result()
{ {
var rs = ReadToEnd(); Exec();
if (!rs.IsSuccess) _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
return _changes;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
ParseLine(line);
_changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path));
return _changes; return _changes;
} }
private void ParseLine(string line) protected override void OnReadline(string line)
{ {
var match = REG_FORMAT().Match(line); var match = REG_FORMAT().Match(line);
if (!match.Success) 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; return;
}
var change = new Models.Change() { Path = match.Groups[2].Value }; var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value; var status = match.Groups[1].Value;
@ -76,6 +57,10 @@ namespace SourceGit.Commands
change.Set(Models.ChangeState.Deleted); change.Set(Models.ChangeState.Deleted);
_changes.Add(change); _changes.Add(change);
break; break;
case 'R':
change.Set(Models.ChangeState.Renamed);
_changes.Add(change);
break;
case 'C': case 'C':
change.Set(Models.ChangeState.Copied); change.Set(Models.ChangeState.Copied);
_changes.Add(change); _changes.Add(change);

View file

@ -29,7 +29,7 @@ namespace SourceGit.Commands
var rs = new Dictionary<string, string>(); var rs = new Dictionary<string, string>();
if (output.IsSuccess) if (output.IsSuccess)
{ {
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
var idx = line.IndexOf('=', StringComparison.Ordinal); var idx = line.IndexOf('=', StringComparison.Ordinal);

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain"; Args = "status -uno --ignore-submodules=dirty --porcelain";
} }
public int Result() public int Result()
@ -16,7 +16,7 @@ namespace SourceGit.Commands
var rs = ReadToEnd(); var rs = ReadToEnd();
if (rs.IsSuccess) if (rs.IsSuccess)
{ {
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
return lines.Length; return lines.Length;
} }

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -28,48 +28,34 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
if (ignoreWhitespace) if (ignoreWhitespace)
Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}"; Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
else if (Models.DiffOption.IgnoreCRAtEOL)
Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
else else
Args = $"diff --no-ext-diff --patch --unified={unified} {opt}"; Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
} }
public Models.DiffResult Result() public Models.DiffResult Result()
{ {
var rs = ReadToEnd(); Exec();
var start = 0;
var end = rs.StdOut.IndexOf('\n', start);
while (end > 0)
{
var line = rs.StdOut.Substring(start, end - start);
ParseLine(line);
start = end + 1; if (_result.IsBinary || _result.IsLFS)
end = rs.StdOut.IndexOf('\n', start);
}
if (start < rs.StdOut.Length)
ParseLine(rs.StdOut.Substring(start));
if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0)
{ {
_result.TextDiff = null; _result.TextDiff = null;
} }
else else
{ {
ProcessInlineHighlights(); ProcessInlineHighlights();
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
if (_result.TextDiff.Lines.Count == 0)
_result.TextDiff = null;
else
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
} }
return _result; return _result;
} }
private void ParseLine(string line) protected override void OnReadline(string line)
{ {
if (_result.IsBinary)
return;
if (line.StartsWith("old mode ", StringComparison.Ordinal)) if (line.StartsWith("old mode ", StringComparison.Ordinal))
{ {
_result.OldMode = line.Substring(9); _result.OldMode = line.Substring(9);
@ -82,17 +68,8 @@ namespace SourceGit.Commands
return; return;
} }
if (line.StartsWith("deleted file mode ", StringComparison.Ordinal)) if (_result.IsBinary)
{
_result.OldMode = line.Substring(18);
return; return;
}
if (line.StartsWith("new file mode ", StringComparison.Ordinal))
{
_result.NewMode = line.Substring(14);
return;
}
if (_result.IsLFS) if (_result.IsLFS)
{ {
@ -105,7 +82,7 @@ namespace SourceGit.Commands
} }
else if (line.StartsWith("-size ", StringComparison.Ordinal)) else if (line.StartsWith("-size ", StringComparison.Ordinal))
{ {
_result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
} }
} }
else if (ch == '+') else if (ch == '+')
@ -116,12 +93,12 @@ namespace SourceGit.Commands
} }
else if (line.StartsWith("+size ", StringComparison.Ordinal)) else if (line.StartsWith("+size ", StringComparison.Ordinal))
{ {
_result.LFSDiff.New.Size = long.Parse(line.AsSpan(6)); _result.LFSDiff.New.Size = long.Parse(line.Substring(6));
} }
} }
else if (line.StartsWith(" size ", StringComparison.Ordinal)) else if (line.StartsWith(" size ", StringComparison.Ordinal))
{ {
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
} }
return; return;
} }
@ -151,8 +128,7 @@ namespace SourceGit.Commands
_oldLine = int.Parse(match.Groups[1].Value); _oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value); _newLine = int.Parse(match.Groups[2].Value);
_last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
_result.TextDiff.Lines.Add(_last);
} }
} }
else else
@ -160,8 +136,7 @@ namespace SourceGit.Commands
if (line.Length == 0) if (line.Length == 0)
{ {
ProcessInlineHighlights(); ProcessInlineHighlights();
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
_result.TextDiff.Lines.Add(_last);
_oldLine++; _oldLine++;
_newLine++; _newLine++;
return; return;
@ -177,8 +152,7 @@ namespace SourceGit.Commands
return; return;
} }
_last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); _deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
_deleted.Add(_last);
_oldLine++; _oldLine++;
} }
else if (ch == '+') else if (ch == '+')
@ -190,8 +164,7 @@ namespace SourceGit.Commands
return; return;
} }
_last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine); _added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
_added.Add(_last);
_newLine++; _newLine++;
} }
else if (ch != '\\') else if (ch != '\\')
@ -202,8 +175,7 @@ namespace SourceGit.Commands
{ {
_oldLine = int.Parse(match.Groups[1].Value); _oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value); _newLine = int.Parse(match.Groups[2].Value);
_last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
_result.TextDiff.Lines.Add(_last);
} }
else else
{ {
@ -214,16 +186,11 @@ namespace SourceGit.Commands
return; return;
} }
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine));
_result.TextDiff.Lines.Add(_last);
_oldLine++; _oldLine++;
_newLine++; _newLine++;
} }
} }
else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal))
{
_last.NoNewLineEndOfFile = true;
}
} }
} }
@ -274,7 +241,6 @@ namespace SourceGit.Commands
private readonly Models.DiffResult _result = new Models.DiffResult(); private readonly Models.DiffResult _result = new Models.DiffResult();
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>(); private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>(); private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private Models.TextDiffLine _last = null;
private int _oldLine = 0; private int _oldLine = 0;
private int _newLine = 0; private int _newLine = 0;
} }

View file

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

View file

@ -17,6 +17,14 @@ namespace SourceGit.Commands
start.CreateNoWindow = true; start.CreateNoWindow = true;
start.WorkingDirectory = repo; 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);
try try
{ {
Process.Start(start); Process.Start(start);
@ -27,7 +35,7 @@ namespace SourceGit.Commands
} }
} }
public static void RunAndWait(string repo, string file, string args, Models.ICommandLog log) public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
{ {
var start = new ProcessStartInfo(); var start = new ProcessStartInfo();
start.FileName = file; start.FileName = file;
@ -40,7 +48,13 @@ namespace SourceGit.Commands
start.StandardErrorEncoding = Encoding.UTF8; start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo; start.WorkingDirectory = repo;
log?.AppendLine($"$ {file} {args}\n"); // 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 proc = new Process() { StartInfo = start };
var builder = new StringBuilder(); var builder = new StringBuilder();
@ -48,14 +62,14 @@ namespace SourceGit.Commands
proc.OutputDataReceived += (_, e) => proc.OutputDataReceived += (_, e) =>
{ {
if (e.Data != null) if (e.Data != null)
log?.AppendLine(e.Data); outputHandler?.Invoke(e.Data);
}; };
proc.ErrorDataReceived += (_, e) => proc.ErrorDataReceived += (_, e) =>
{ {
if (e.Data != null) if (e.Data != null)
{ {
log?.AppendLine(e.Data); outputHandler?.Invoke(e.Data);
builder.AppendLine(e.Data); builder.AppendLine(e.Data);
} }
}; };

View file

@ -1,11 +1,15 @@
namespace SourceGit.Commands using System;
namespace SourceGit.Commands
{ {
public class Fetch : Command public class Fetch : Command
{ {
public Fetch(string repo, string remote, bool noTags, bool force) public Fetch(string repo, string remote, bool noTags, bool force, Action<string> outputHandler)
{ {
_outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "fetch --progress --verbose "; Args = "fetch --progress --verbose ";
@ -20,12 +24,21 @@
Args += remote; Args += remote;
} }
public Fetch(string repo, Models.Branch local, Models.Branch remote) public Fetch(string repo, Models.Branch local, Models.Branch remote, Action<string> outputHandler)
{ {
_outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey"); SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey");
Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}"; Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}";
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
} }
} }

View file

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

View file

@ -1,12 +1,23 @@
namespace SourceGit.Commands using System;
namespace SourceGit.Commands
{ {
public class GC : Command public class GC : Command
{ {
public GC(string repo) public GC(string repo, Action<string> outputHandler)
{ {
_outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true;
Args = "gc --prune=now"; Args = "gc --prune=now";
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
} }
} }

View file

@ -1,12 +1,52 @@
using System.Text; using System;
using System.Collections.Generic;
using Avalonia.Threading; using Avalonia.Threading;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public static class GitFlow public static class GitFlow
{ {
public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log) public class BranchDetectResult
{ {
public bool IsGitFlowBranch { get; set; } = false;
public string Type { get; set; } = string.Empty;
public string Prefix { get; set; } = string.Empty;
}
public static bool IsEnabled(string repo, List<Models.Branch> branches)
{
var localBrancheNames = new HashSet<string>();
foreach (var branch in branches)
{
if (branch.IsLocal)
localBrancheNames.Add(branch.Name);
}
var config = new Config(repo).ListAll();
if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
return false;
if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop))
return false;
return config.ContainsKey("gitflow.prefix.feature") &&
config.ContainsKey("gitflow.prefix.release") &&
config.ContainsKey("gitflow.prefix.hotfix");
}
public static bool Init(string repo, List<Models.Branch> branches, string master, string develop, string feature, string release, string hotfix, string version)
{
var current = branches.Find(x => x.IsCurrent);
var masterBranch = branches.Find(x => x.Name == master);
if (masterBranch == null && current != null)
Branch.Create(repo, master, current.Head);
var devBranch = branches.Find(x => x.Name == develop);
if (devBranch == null && current != null)
Branch.Create(repo, develop, current.Head);
var config = new Config(repo); var config = new Config(repo);
config.Set("gitflow.branch.master", master); config.Set("gitflow.branch.master", master);
config.Set("gitflow.branch.develop", develop); config.Set("gitflow.branch.develop", develop);
@ -21,72 +61,104 @@ namespace SourceGit.Commands
init.WorkingDirectory = repo; init.WorkingDirectory = repo;
init.Context = repo; init.Context = repo;
init.Args = "flow init -d"; init.Args = "flow init -d";
init.Log = log;
return init.Exec(); return init.Exec();
} }
public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) public static string GetPrefix(string repo, string type)
{ {
return new Config(repo).Get($"gitflow.prefix.{type}");
}
public static BranchDetectResult DetectType(string repo, List<Models.Branch> branches, string branch)
{
var rs = new BranchDetectResult();
var localBrancheNames = new HashSet<string>();
foreach (var b in branches)
{
if (b.IsLocal)
localBrancheNames.Add(b.Name);
}
var config = new Config(repo).ListAll();
if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
return rs;
if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop))
return rs;
if (!config.TryGetValue("gitflow.prefix.feature", out var feature) ||
!config.TryGetValue("gitflow.prefix.release", out var release) ||
!config.TryGetValue("gitflow.prefix.hotfix", out var hotfix))
return rs;
if (branch.StartsWith(feature, StringComparison.Ordinal))
{
rs.IsGitFlowBranch = true;
rs.Type = "feature";
rs.Prefix = feature;
}
else if (branch.StartsWith(release, StringComparison.Ordinal))
{
rs.IsGitFlowBranch = true;
rs.Type = "release";
rs.Prefix = release;
}
else if (branch.StartsWith(hotfix, StringComparison.Ordinal))
{
rs.IsGitFlowBranch = true;
rs.Type = "hotfix";
rs.Prefix = hotfix;
}
return rs;
}
public static bool Start(string repo, string type, string name)
{
if (!SUPPORTED_BRANCH_TYPES.Contains(type))
{
Dispatcher.UIThread.Post(() =>
{
App.RaiseException(repo, "Bad branch type!!!");
});
return false;
}
var start = new Command(); var start = new Command();
start.WorkingDirectory = repo; start.WorkingDirectory = repo;
start.Context = repo; start.Context = repo;
start.Args = $"flow {type} start {name}";
switch (type)
{
case Models.GitFlowBranchType.Feature:
start.Args = $"flow feature start {name}";
break;
case Models.GitFlowBranchType.Release:
start.Args = $"flow release start {name}";
break;
case Models.GitFlowBranchType.Hotfix:
start.Args = $"flow hotfix start {name}";
break;
default:
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
return false;
}
start.Log = log;
return start.Exec(); return start.Exec();
} }
public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) public static bool Finish(string repo, string type, string name, bool keepBranch)
{ {
var builder = new StringBuilder(); if (!SUPPORTED_BRANCH_TYPES.Contains(type))
builder.Append("flow ");
switch (type)
{ {
case Models.GitFlowBranchType.Feature: Dispatcher.UIThread.Post(() =>
builder.Append("feature"); {
break; App.RaiseException(repo, "Bad branch type!!!");
case Models.GitFlowBranchType.Release: });
builder.Append("release");
break; return false;
case Models.GitFlowBranchType.Hotfix:
builder.Append("hotfix");
break;
default:
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
return false;
} }
builder.Append(" finish "); var option = keepBranch ? "-k" : string.Empty;
if (squash)
builder.Append("--squash ");
if (push)
builder.Append("--push ");
if (keepBranch)
builder.Append("-k ");
builder.Append(name);
var finish = new Command(); var finish = new Command();
finish.WorkingDirectory = repo; finish.WorkingDirectory = repo;
finish.Context = repo; finish.Context = repo;
finish.Args = builder.ToString(); finish.Args = $"flow {type} finish {option} {name}";
finish.Log = log;
return finish.Exec(); return finish.Exec();
} }
private static readonly List<string> SUPPORTED_BRANCH_TYPES = new List<string>()
{
"feature",
"release",
"bugfix",
"hotfix",
"support",
};
} }
} }

View file

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

View file

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

View file

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

@ -10,10 +10,5 @@
Context = repo; Context = repo;
Args = $"diff -a --ignore-cr-at-eol --check {opt}"; Args = $"diff -a --ignore-cr-at-eol --check {opt}";
} }
public bool Result()
{
return ReadToEnd().IsSuccess;
}
} }
} }

View file

@ -10,15 +10,23 @@ namespace SourceGit.Commands
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] [GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK(); private static partial Regex REG_LOCK();
private class SubCmd : Command class SubCmd : Command
{ {
public SubCmd(string repo, string args, Models.ICommandLog log) public SubCmd(string repo, string args, Action<string> onProgress)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = args; Args = args;
Log = log; TraitErrorAsOutput = true;
_outputHandler = onProgress;
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
} }
public LFS(string repo) public LFS(string repo)
@ -36,35 +44,35 @@ namespace SourceGit.Commands
return content.Contains("git lfs pre-push"); return content.Contains("git lfs pre-push");
} }
public bool Install(Models.ICommandLog log) public bool Install()
{ {
return new SubCmd(_repo, "lfs install --local", log).Exec(); return new SubCmd(_repo, "lfs install --local", null).Exec();
} }
public bool Track(string pattern, bool isFilenameMode, Models.ICommandLog log) public bool Track(string pattern, bool isFilenameMode = false)
{ {
var opt = isFilenameMode ? "--filename" : ""; var opt = isFilenameMode ? "--filename" : "";
return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", log).Exec(); return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", null).Exec();
} }
public void Fetch(string remote, Models.ICommandLog log) public void Fetch(string remote, Action<string> outputHandler)
{ {
new SubCmd(_repo, $"lfs fetch {remote}", log).Exec(); new SubCmd(_repo, $"lfs fetch {remote}", outputHandler).Exec();
} }
public void Pull(string remote, Models.ICommandLog log) public void Pull(string remote, Action<string> outputHandler)
{ {
new SubCmd(_repo, $"lfs pull {remote}", log).Exec(); new SubCmd(_repo, $"lfs pull {remote}", outputHandler).Exec();
} }
public void Push(string remote, Models.ICommandLog log) public void Push(string remote, Action<string> outputHandler)
{ {
new SubCmd(_repo, $"lfs push {remote}", log).Exec(); new SubCmd(_repo, $"lfs push {remote}", outputHandler).Exec();
} }
public void Prune(Models.ICommandLog log) public void Prune(Action<string> outputHandler)
{ {
new SubCmd(_repo, "lfs prune", log).Exec(); new SubCmd(_repo, "lfs prune", outputHandler).Exec();
} }
public List<Models.LFSLock> Locks(string remote) public List<Models.LFSLock> Locks(string remote)
@ -74,7 +82,7 @@ namespace SourceGit.Commands
var rs = cmd.ReadToEnd(); var rs = cmd.ReadToEnd();
if (rs.IsSuccess) if (rs.IsSuccess)
{ {
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
var match = REG_LOCK().Match(line); var match = REG_LOCK().Match(line);
@ -93,21 +101,21 @@ namespace SourceGit.Commands
return locks; return locks;
} }
public bool Lock(string remote, string file, Models.ICommandLog log) public bool Lock(string remote, string file)
{ {
return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", log).Exec(); return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", null).Exec();
} }
public bool Unlock(string remote, string file, bool force, Models.ICommandLog log) public bool Unlock(string remote, string file, bool force)
{ {
var opt = force ? "-f" : ""; var opt = force ? "-f" : "";
return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", log).Exec(); return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", null).Exec();
} }
public bool Unlock(string remote, long id, bool force, Models.ICommandLog log) public bool Unlock(string remote, long id, bool force)
{ {
var opt = force ? "-f" : ""; var opt = force ? "-f" : "";
return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", log).Exec(); return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", null).Exec();
} }
private readonly string _repo; private readonly string _repo;

View file

@ -1,30 +1,26 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public class Merge : Command public class Merge : Command
{ {
public Merge(string repo, string source, string mode, bool edit) public Merge(string repo, string source, string mode, Action<string> outputHandler)
{ {
_outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Editor = EditorType.CoreEditor; TraitErrorAsOutput = true;
Args = $"merge --progress {source} {mode}";
var builder = new StringBuilder();
builder.Append("merge --progress ");
builder.Append(edit ? "--edit " : "--no-edit ");
builder.Append(source);
builder.Append(' ');
builder.Append(mode);
Args = builder.ToString();
} }
public Merge(string repo, List<string> targets, bool autoCommit, string strategy) public Merge(string repo, List<string> targets, bool autoCommit, string strategy, Action<string> outputHandler)
{ {
_outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true;
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("merge --progress "); builder.Append("merge --progress ");
@ -41,5 +37,12 @@ namespace SourceGit.Commands
Args = builder.ToString(); Args = builder.ToString();
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
} }
} }

View file

@ -13,18 +13,15 @@ namespace SourceGit.Commands
cmd.Context = repo; cmd.Context = repo;
cmd.RaiseError = true; 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) if (toolType == 0)
{ {
cmd.Args = $"mergetool {fileArg}"; cmd.Args = $"mergetool \"{file}\"";
return cmd.Exec(); return cmd.Exec();
} }
if (!File.Exists(toolPath)) if (!File.Exists(toolPath))
{ {
Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!")); Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!"));
return false; return false;
} }
@ -35,7 +32,7 @@ namespace SourceGit.Commands
return false; 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 {fileArg}"; 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}\"";
return cmd.Exec(); return cmd.Exec();
} }
@ -54,7 +51,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath)) if (!File.Exists(toolPath))
{ {
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!")); Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!"));
return false; return false;
} }

View file

@ -1,18 +1,31 @@
namespace SourceGit.Commands using System;
namespace SourceGit.Commands
{ {
public class Pull : Command public class Pull : Command
{ {
public Pull(string repo, string remote, string branch, bool useRebase) public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
{ {
_outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "pull --verbose --progress "; Args = "pull --verbose --progress --tags ";
if (useRebase) if (useRebase)
Args += "--rebase=true "; Args += "--rebase ";
if (noTags)
Args += "--no-tags ";
Args += $"{remote} {branch}"; Args += $"{remote} {branch}";
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
} }
} }

View file

@ -1,11 +1,16 @@
namespace SourceGit.Commands using System;
namespace SourceGit.Commands
{ {
public class Push : Command public class Push : Command
{ {
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force) public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force, Action<string> onProgress)
{ {
_outputHandler = onProgress;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "push --progress --verbose "; Args = "push --progress --verbose ";
@ -21,7 +26,7 @@
Args += $"{remote} {local}:{remoteBranch}"; Args += $"{remote} {local}:{remoteBranch}";
} }
public Push(string repo, string remote, string refname, bool isDelete) public Push(string repo, string remote, string tag, bool isDelete)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
@ -31,7 +36,14 @@
if (isDelete) if (isDelete)
Args += "--delete "; Args += "--delete ";
Args += $"{remote} {refname}"; Args += $"{remote} refs/tags/{tag}";
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
} }
} }

View file

@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryAssumeUnchangedFiles : Command
{
[GeneratedRegex(@"^(\w)\s+(.+)$")]
private static partial Regex REG_PARSE();
public QueryAssumeUnchangedFiles(string repo)
{
WorkingDirectory = repo;
Args = "ls-files -v";
RaiseError = false;
}
public List<string> Result()
{
var outs = new List<string>();
var rs = ReadToEnd();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_PARSE().Match(line);
if (!match.Success)
continue;
if (match.Groups[1].Value == "h")
outs.Add(match.Groups[2].Value);
}
return outs;
}
}
}

View file

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

View file

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
@ -10,26 +9,22 @@ namespace SourceGit.Commands
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
_commit = commit; _commit = commit;
Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}"; Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
} }
public List<string> Result() public IEnumerable<string> Result()
{ {
var rs = ReadToEnd(); Exec();
var outs = new List<string>(); return _lines;
if (rs.IsSuccess) }
{
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains(_commit))
outs.Add(line.Substring(0, 40));
}
}
return outs; protected override void OnReadline(string line)
{
if (line.Contains(_commit))
_lines.Add(line.Substring(0, 40));
} }
private string _commit; private string _commit;
private List<string> _lines = new List<string>();
} }
} }

View file

@ -6,7 +6,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"show --no-show-signature --format=%B -s {sha}"; Args = $"show --no-show-signature --pretty=format:%B -s {sha}";
} }
public string Result() public string Result()

View file

@ -7,7 +7,7 @@
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
const string baseArgs = "show --no-show-signature --format=%G?%n%GS%n%GK -s"; const string baseArgs = "show --no-show-signature --pretty=format:\"%G?%n%GS%n%GK\" -s";
const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null"; const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null";
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}"; Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
} }
@ -18,7 +18,7 @@
if (!rs.IsSuccess) if (!rs.IsSuccess)
return null; return null;
var raw = rs.StdOut.Trim().ReplaceLineEndings("\n"); var raw = rs.StdOut.Trim();
if (raw.Length <= 1) if (raw.Length <= 1)
return null; return null;
@ -29,6 +29,7 @@
Signer = lines[1], Signer = lines[1],
Key = lines[2] Key = lines[2]
}; };
} }
} }
} }

View file

@ -10,7 +10,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
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}"; 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}";
_findFirstMerged = needFindHead; _findFirstMerged = needFindHead;
} }
@ -26,12 +26,16 @@ namespace SourceGit.Commands
{ {
search += $"-i --committer=\"{filter}\""; search += $"-i --committer=\"{filter}\"";
} }
else if (method == Models.CommitSearchMethod.ByMessage) else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
{ {
var argsBuilder = new StringBuilder(); var argsBuilder = new StringBuilder();
argsBuilder.Append(search); argsBuilder.Append(search);
var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries); var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words) foreach (var word in words)
{ {
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal); var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
@ -41,18 +45,10 @@ namespace SourceGit.Commands
search = argsBuilder.ToString(); search = argsBuilder.ToString();
} }
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
{
search = $"-G\"{filter}\"";
}
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {search}"; Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
_findFirstMerged = false; _findFirstMerged = false;
} }
@ -128,7 +124,7 @@ namespace SourceGit.Commands
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\""; Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd(); var rs = ReadToEnd();
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0) if (shas.Length == 0)
return; return;

View file

@ -3,18 +3,18 @@ using System.Collections.Generic;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public class QueryCommitsForInteractiveRebase : Command public class QueryCommitsWithFullMessage : Command
{ {
public QueryCommitsForInteractiveRebase(string repo, string on) public QueryCommitsWithFullMessage(string repo, string args)
{ {
_boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----"; _boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----";
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
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"; 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}";
} }
public List<Models.InteractiveCommit> Result() public List<Models.CommitWithMessage> Result()
{ {
var rs = ReadToEnd(); var rs = ReadToEnd();
if (!rs.IsSuccess) if (!rs.IsSuccess)
@ -29,7 +29,7 @@ namespace SourceGit.Commands
switch (nextPartIdx) switch (nextPartIdx)
{ {
case 0: case 0:
_current = new Models.InteractiveCommit(); _current = new Models.CommitWithMessage();
_current.Commit.SHA = line; _current.Commit.SHA = line;
_commits.Add(_current); _commits.Add(_current);
break; break;
@ -52,7 +52,7 @@ namespace SourceGit.Commands
_current.Commit.CommitterTime = ulong.Parse(line); _current.Commit.CommitterTime = ulong.Parse(line);
break; break;
default: default:
var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal); var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
if (boundary > end) if (boundary > end)
{ {
_current.Message = rs.StdOut.Substring(start, boundary - start - 1); _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)); _current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
} }
private List<Models.InteractiveCommit> _commits = []; private List<Models.CommitWithMessage> _commits = new List<Models.CommitWithMessage>();
private Models.InteractiveCommit _current = null; private Models.CommitWithMessage _current = null;
private readonly string _boundary; private string _boundary = "";
} }
} }

View file

@ -35,39 +35,5 @@ namespace SourceGit.Commands
return stream; return stream;
} }
public static Stream FromLFS(string repo, string oid, long size)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitExecutable;
starter.Arguments = $"lfs smudge";
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardInput = true;
starter.RedirectStandardOutput = true;
var stream = new MemoryStream();
try
{
var proc = new Process() { StartInfo = starter };
proc.Start();
proc.StandardInput.WriteLine("version https://git-lfs.github.com/spec/v1");
proc.StandardInput.WriteLine($"oid sha256:{oid}");
proc.StandardInput.WriteLine($"size {size}");
proc.StandardOutput.BaseStream.CopyTo(stream);
proc.WaitForExit();
proc.Close();
stream.Position = 0;
}
catch (Exception e)
{
App.RaiseException(repo, $"Failed to query file content: {e}");
}
return stream;
}
} }
} }

View file

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

View file

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

@ -1,9 +1,6 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Avalonia.Threading;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public partial class QueryLocalChanges : Command public partial class QueryLocalChanges : Command
@ -16,150 +13,144 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
} }
public List<Models.Change> Result() public List<Models.Change> Result()
{ {
var outs = new List<Models.Change>(); Exec();
var rs = ReadToEnd(); return _changes;
if (!rs.IsSuccess)
{
Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr));
return outs;
}
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
continue;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status)
{
case " M":
change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
break;
case " T":
change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
break;
case " A":
change.Set(Models.ChangeState.None, Models.ChangeState.Added);
break;
case " D":
change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
break;
case " R":
change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
break;
case " C":
change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "MM":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
break;
case "MT":
change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
break;
case "MD":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
case "TM":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
break;
case "TT":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
break;
case "TD":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
break;
case "A":
change.Set(Models.ChangeState.Added);
break;
case "AM":
change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
break;
case "AT":
change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
break;
case "AD":
change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "R":
change.Set(Models.ChangeState.Renamed);
break;
case "RM":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
break;
case "RT":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
break;
case "RD":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "CM":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
break;
case "CT":
change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
break;
case "CD":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
break;
case "DD":
change.ConflictReason = Models.ConflictReason.BothDeleted;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "AU":
change.ConflictReason = Models.ConflictReason.AddedByUs;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UD":
change.ConflictReason = Models.ConflictReason.DeletedByThem;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UA":
change.ConflictReason = Models.ConflictReason.AddedByThem;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "DU":
change.ConflictReason = Models.ConflictReason.DeletedByUs;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "AA":
change.ConflictReason = Models.ConflictReason.BothAdded;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UU":
change.ConflictReason = Models.ConflictReason.BothModified;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "??":
change.Set(Models.ChangeState.None, Models.ChangeState.Untracked);
break;
}
if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None)
outs.Add(change);
}
return outs;
} }
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)
{
case " M":
change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
break;
case " T":
change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
break;
case " A":
change.Set(Models.ChangeState.None, Models.ChangeState.Added);
break;
case " D":
change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
break;
case " R":
change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
break;
case " C":
change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "MM":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
break;
case "MT":
change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
break;
case "MD":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
case "TM":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
break;
case "TT":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
break;
case "TD":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
break;
case "A":
change.Set(Models.ChangeState.Added);
break;
case "AM":
change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
break;
case "AT":
change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
break;
case "AD":
change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "R":
change.Set(Models.ChangeState.Renamed);
break;
case "RM":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
break;
case "RT":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
break;
case "RD":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "CM":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
break;
case "CT":
change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
break;
case "CD":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
break;
case "DR":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed);
break;
case "DC":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied);
break;
case "DD":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted);
break;
case "AU":
change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged);
break;
case "UD":
change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted);
break;
case "UA":
change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added);
break;
case "DU":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged);
break;
case "AA":
change.Set(Models.ChangeState.Added, Models.ChangeState.Added);
break;
case "UU":
change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged);
break;
case "??":
change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked);
break;
default:
return;
}
_changes.Add(change);
}
private readonly List<Models.Change> _changes = new List<Models.Change>();
} }
} }

View file

@ -20,7 +20,7 @@ namespace SourceGit.Commands
if (!output.IsSuccess) if (!output.IsSuccess)
return rs; return rs;
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = output.StdOut.Split('\n');
foreach (var line in lines) foreach (var line in lines)
{ {
if (line.EndsWith("/HEAD", StringComparison.Ordinal)) if (line.EndsWith("/HEAD", StringComparison.Ordinal))

View file

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands namespace SourceGit.Commands
@ -18,31 +17,27 @@ namespace SourceGit.Commands
public List<Models.Remote> Result() public List<Models.Remote> Result()
{ {
var outs = new List<Models.Remote>(); Exec();
var rs = ReadToEnd(); return _loaded;
if (!rs.IsSuccess)
return outs;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_REMOTE().Match(line);
if (!match.Success)
continue;
var remote = new Models.Remote()
{
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (outs.Find(x => x.Name == remote.Name) != null)
continue;
outs.Add(remote);
}
return outs;
} }
protected override void OnReadline(string line)
{
var match = REG_REMOTE().Match(line);
if (!match.Success)
return;
var remote = new Models.Remote()
{
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (_loaded.Find(x => x.Name == remote.Name) != null)
return;
_loaded.Add(remote);
}
private readonly List<Models.Remote> _loaded = new List<Models.Remote>();
} }
} }

View file

@ -1,21 +0,0 @@
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,6 +1,4 @@
using System.Collections.Generic; namespace SourceGit.Commands
namespace SourceGit.Commands
{ {
public class QueryRevisionFileNames : Command public class QueryRevisionFileNames : Command
{ {
@ -11,17 +9,13 @@ namespace SourceGit.Commands
Args = $"ls-tree -r -z --name-only {revision}"; Args = $"ls-tree -r -z --name-only {revision}";
} }
public List<string> Result() public string[] Result()
{ {
var rs = ReadToEnd(); var rs = ReadToEnd();
if (!rs.IsSuccess) if (rs.IsSuccess)
return []; return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); return [];
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; WorkingDirectory = repo;
Context = repo; Context = repo;
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}"; 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}";
} }
public Models.Commit Result() public Models.Commit Result()

View file

@ -6,87 +6,87 @@ namespace SourceGit.Commands
{ {
public partial class QueryStagedChangesWithAmend : Command public partial class QueryStagedChangesWithAmend : Command
{ {
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")] [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")]
private static partial Regex REG_FORMAT1(); private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")] [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")]
private static partial Regex REG_FORMAT2(); private static partial Regex REG_FORMAT2();
public QueryStagedChangesWithAmend(string repo, string parent) public QueryStagedChangesWithAmend(string repo)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff-index --cached -M {parent}"; Args = "diff-index --cached -M HEAD^";
_parent = parent;
} }
public List<Models.Change> Result() public List<Models.Change> Result()
{ {
var rs = ReadToEnd(); var rs = ReadToEnd();
if (!rs.IsSuccess) if (rs.IsSuccess)
return [];
var changes = new List<Models.Change>();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{ {
var match = REG_FORMAT2().Match(line); var changes = new List<Models.Change>();
if (match.Success) var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{ {
var change = new Models.Change() var match = REG_FORMAT2().Match(line);
if (match.Success)
{ {
Path = match.Groups[3].Value, var change = new Models.Change()
DataForAmend = new Models.ChangeDataForAmend()
{ {
FileMode = match.Groups[1].Value, Path = match.Groups[3].Value,
ObjectHash = match.Groups[2].Value, DataForAmend = new Models.ChangeDataForAmend()
ParentSHA = _parent, {
}, FileMode = match.Groups[1].Value,
}; ObjectHash = match.Groups[2].Value,
change.Set(Models.ChangeState.Renamed); },
changes.Add(change); };
continue; change.Set(Models.ChangeState.Renamed);
} changes.Add(change);
continue;
match = REG_FORMAT1().Match(line); }
if (match.Success)
{ match = REG_FORMAT1().Match(line);
var change = new Models.Change() if (match.Success)
{ {
Path = match.Groups[4].Value, var change = new Models.Change()
DataForAmend = new Models.ChangeDataForAmend() {
{ Path = match.Groups[4].Value,
FileMode = match.Groups[1].Value, DataForAmend = new Models.ChangeDataForAmend()
ObjectHash = match.Groups[2].Value, {
ParentSHA = _parent, FileMode = match.Groups[1].Value,
}, ObjectHash = match.Groups[2].Value,
}; },
};
var type = match.Groups[3].Value;
switch (type) var type = match.Groups[3].Value;
{ switch (type)
case "A": {
change.Set(Models.ChangeState.Added); case "A":
break; change.Set(Models.ChangeState.Added);
case "C": break;
change.Set(Models.ChangeState.Copied); case "C":
break; change.Set(Models.ChangeState.Copied);
case "D": break;
change.Set(Models.ChangeState.Deleted); case "D":
break; change.Set(Models.ChangeState.Deleted);
case "M": break;
change.Set(Models.ChangeState.Modified); case "M":
break; change.Set(Models.ChangeState.Modified);
case "T": break;
change.Set(Models.ChangeState.TypeChanged); case "T":
break; change.Set(Models.ChangeState.TypeChanged);
break;
case "U":
change.Set(Models.ChangeState.Unmerged);
break;
}
changes.Add(change);
} }
changes.Add(change);
} }
return changes;
} }
return changes; return [];
} }
private readonly string _parent;
} }
} }

View file

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

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

View file

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

View file

@ -19,7 +19,7 @@ namespace SourceGit.Commands
if (!rs.IsSuccess) if (!rs.IsSuccess)
return status; return status;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
if (line[0] == '>') if (line[0] == '>')

View file

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryUpdatableSubmodules : Command
{
[GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")]
private static partial Regex REG_FORMAT_STATUS();
public QueryUpdatableSubmodules(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "submodule status";
}
public List<string> Result()
{
var submodules = new List<string>();
var rs = ReadToEnd();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_STATUS().Match(line);
if (match.Success)
{
var stat = match.Groups[1].Value;
var path = match.Groups[3].Value;
if (!stat.StartsWith(' '))
submodules.Add(path);
}
}
return submodules;
}
}
}

View file

@ -1,7 +1,33 @@
namespace SourceGit.Commands using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{ {
public class Reset : Command public class Reset : Command
{ {
public Reset(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "reset";
}
public Reset(string repo, List<Models.Change> changes)
{
WorkingDirectory = repo;
Context = repo;
var builder = new StringBuilder();
builder.Append("reset --");
foreach (var c in changes)
{
builder.Append(" \"");
builder.Append(c.Path);
builder.Append("\"");
}
Args = builder.ToString();
}
public Reset(string repo, string revision, string mode) public Reset(string repo, string revision, string mode)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;

View file

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

View file

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

View file

@ -82,7 +82,7 @@ namespace SourceGit.Commands
public bool Pop(string name) public bool Pop(string name)
{ {
Args = $"stash pop -q --index \"{name}\""; Args = $"stash pop -q \"{name}\"";
return Exec(); return Exec();
} }

View file

@ -1,4 +1,4 @@
using System; using System;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
@ -8,7 +8,7 @@ namespace SourceGit.Commands
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE"; Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
} }
public Models.Statistics Result() public Models.Statistics Result()
@ -40,7 +40,7 @@ namespace SourceGit.Commands
if (dateEndIdx == -1) if (dateEndIdx == -1)
return; return;
var dateStr = line.AsSpan(0, dateEndIdx); var dateStr = line.Substring(0, dateEndIdx);
if (double.TryParse(dateStr, out var date)) if (double.TryParse(dateStr, out var date))
statistics.AddCommit(line.Substring(dateEndIdx + 1), date); statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
} }

View file

@ -1,5 +1,4 @@
using System.Collections.Generic; using System;
using System.Text;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
@ -11,9 +10,10 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
} }
public bool Add(string url, string relativePath, bool recursive) public bool Add(string url, string relativePath, bool recursive, Action<string> outputHandler)
{ {
Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\""; _outputHandler = outputHandler;
Args = $"submodule add {url} \"{relativePath}\"";
if (!Exec()) if (!Exec())
return false; return false;
@ -29,38 +29,38 @@ namespace SourceGit.Commands
} }
} }
public bool Update(List<string> modules, bool init, bool recursive, bool useRemote = false) public bool Update(string module, bool init, bool recursive, bool useRemote, Action<string> outputHandler)
{ {
var builder = new StringBuilder(); Args = "submodule update";
builder.Append("submodule update");
if (init) if (init)
builder.Append(" --init"); Args += " --init";
if (recursive) if (recursive)
builder.Append(" --recursive"); Args += " --recursive";
if (useRemote) if (useRemote)
builder.Append(" --remote"); Args += " --remote";
if (modules.Count > 0) if (!string.IsNullOrEmpty(module))
{ Args += $" -- \"{module}\"";
builder.Append(" --");
foreach (var module in modules)
builder.Append($" \"{module}\"");
}
Args = builder.ToString(); _outputHandler = outputHandler;
return Exec(); return Exec();
} }
public bool Deinit(string module, bool force) public bool Delete(string relativePath)
{ {
Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\""; Args = $"submodule deinit -f \"{relativePath}\"";
if (!Exec())
return false;
Args = $"rm -rf \"{relativePath}\"";
return Exec(); return Exec();
} }
public bool Delete(string module) protected override void OnReadline(string line)
{ {
Args = $"rm -rf \"{module}\""; _outputHandler?.Invoke(line);
return Exec();
} }
private Action<string> _outputHandler;
} }
} }

View file

@ -1,51 +1,59 @@
using System.IO; using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands namespace SourceGit.Commands
{ {
public static class Tag public static class Tag
{ {
public static bool Add(string repo, string name, string basedOn, Models.ICommandLog log) public static bool Add(string repo, string name, string basedOn)
{ {
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"tag --no-sign {name} {basedOn}"; cmd.Args = $"tag {name} {basedOn}";
cmd.Log = log;
return cmd.Exec(); return cmd.Exec();
} }
public static bool Add(string repo, string name, string basedOn, string message, bool sign, Models.ICommandLog log) public static bool Add(string repo, string name, string basedOn, string message, bool sign)
{ {
var param = sign ? "--sign -a" : "--no-sign -a"; var param = sign ? "--sign -a" : "--no-sign -a";
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"tag {param} {name} {basedOn} "; cmd.Args = $"tag {param} {name} {basedOn} ";
cmd.Log = log;
if (!string.IsNullOrEmpty(message)) if (!string.IsNullOrEmpty(message))
{ {
string tmp = Path.GetTempFileName(); string tmp = Path.GetTempFileName();
File.WriteAllText(tmp, message); File.WriteAllText(tmp, message);
cmd.Args += $"-F \"{tmp}\""; cmd.Args += $"-F \"{tmp}\"";
}
var succ = cmd.Exec(); else
File.Delete(tmp); {
return succ; cmd.Args += $"-m {name}";
} }
cmd.Args += $"-m {name}";
return cmd.Exec(); return cmd.Exec();
} }
public static bool Delete(string repo, string name, Models.ICommandLog log) public static bool Delete(string repo, string name, List<Models.Remote> remotes)
{ {
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
cmd.Args = $"tag --delete {name}"; cmd.Args = $"tag --delete {name}";
cmd.Log = log; if (!cmd.Exec())
return cmd.Exec(); return false;
if (remotes != null)
{
foreach (var r in remotes)
{
new Push(repo, r.Name, name, true).Exec();
}
}
return true;
} }
} }
} }

View file

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

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

@ -0,0 +1,23 @@
using System;
namespace SourceGit.Commands
{
public class UpdateRef : Command
{
public UpdateRef(string repo, string refName, string toRevision, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
Args = $"update-ref {refName} {toRevision}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -21,7 +21,7 @@ namespace SourceGit.Commands
var last = null as Models.Worktree; var last = null as Models.Worktree;
if (rs.IsSuccess) if (rs.IsSuccess)
{ {
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
if (line.StartsWith("worktree ", StringComparison.Ordinal)) if (line.StartsWith("worktree ", StringComparison.Ordinal))
@ -56,7 +56,7 @@ namespace SourceGit.Commands
return worktrees; return worktrees;
} }
public bool Add(string fullpath, string name, bool createNew, string tracking) public bool Add(string fullpath, string name, bool createNew, string tracking, Action<string> outputHandler)
{ {
Args = "worktree add "; Args = "worktree add ";
@ -78,12 +78,14 @@ namespace SourceGit.Commands
else if (!string.IsNullOrEmpty(name) && !createNew) else if (!string.IsNullOrEmpty(name) && !createNew)
Args += name; Args += name;
_outputHandler = outputHandler;
return Exec(); return Exec();
} }
public bool Prune() public bool Prune(Action<string> outputHandler)
{ {
Args = "worktree prune -v"; Args = "worktree prune -v";
_outputHandler = outputHandler;
return Exec(); return Exec();
} }
@ -99,14 +101,22 @@ namespace SourceGit.Commands
return Exec(); return Exec();
} }
public bool Remove(string fullpath, bool force) public bool Remove(string fullpath, bool force, Action<string> outputHandler)
{ {
if (force) if (force)
Args = $"worktree remove -f \"{fullpath}\""; Args = $"worktree remove -f \"{fullpath}\"";
else else
Args = $"worktree remove \"{fullpath}\""; Args = $"worktree remove \"{fullpath}\"";
_outputHandler = outputHandler;
return Exec(); return Exec();
} }
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler = null;
} }
} }

View file

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

View file

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

View file

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

View file

@ -81,8 +81,5 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<string, bool> ContainsSpaces = public static readonly FuncValueConverter<string, bool> ContainsSpaces =
new FuncValueConverter<string, bool>(v => v != null && v.Contains(' ')); 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,22 +2,14 @@
{ {
public class ApplyWhiteSpaceMode 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 Name { get; set; }
public string Desc { get; set; } public string Desc { get; set; }
public string Arg { get; set; } public string Arg { get; set; }
public ApplyWhiteSpaceMode(string n, string d, string a) public ApplyWhiteSpaceMode(string n, string d, string a)
{ {
Name = n; Name = App.Text(n);
Desc = d; Desc = App.Text(d);
Arg = a; Arg = a;
} }
} }

View file

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

View file

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
public enum BisectState
{
None = 0,
WaitingForRange,
Detecting,
}
[Flags]
public enum BisectCommitFlag
{
None = 0,
Good = 1 << 0,
Bad = 1 << 1,
}
public class Bisect
{
public HashSet<string> Bads
{
get;
set;
} = [];
public HashSet<string> Goods
{
get;
set;
} = [];
}
}

View file

@ -23,17 +23,10 @@ namespace SourceGit.Models
} }
} }
public enum BranchSortMode
{
Name = 0,
CommitterDate,
}
public class Branch public class Branch
{ {
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; set; } public string FullName { get; set; }
public ulong CommitterDate { get; set; }
public string Head { get; set; } public string Head { get; set; }
public bool IsLocal { get; set; } public bool IsLocal { get; set; }
public bool IsCurrent { get; set; } public bool IsCurrent { get; set; }
@ -41,7 +34,7 @@ namespace SourceGit.Models
public string Upstream { get; set; } public string Upstream { get; set; }
public BranchTrackStatus TrackStatus { get; set; } public BranchTrackStatus TrackStatus { get; set; }
public string Remote { get; set; } public string Remote { get; set; }
public bool IsUpstreamGone { get; set; } public bool IsUpsteamGone { get; set; }
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
} }

View file

@ -18,27 +18,14 @@ namespace SourceGit.Models
Deleted, Deleted,
Renamed, Renamed,
Copied, Copied,
Untracked, Unmerged,
Conflicted, Untracked
}
public enum ConflictReason
{
None,
BothDeleted,
AddedByUs,
DeletedByThem,
AddedByThem,
DeletedByUs,
BothAdded,
BothModified,
} }
public class ChangeDataForAmend public class ChangeDataForAmend
{ {
public string FileMode { get; set; } = ""; public string FileMode { get; set; } = "";
public string ObjectHash { get; set; } = ""; public string ObjectHash { get; set; } = "";
public string ParentSHA { get; set; } = "";
} }
public class Change public class Change
@ -48,14 +35,20 @@ namespace SourceGit.Models
public string Path { get; set; } = ""; public string Path { get; set; } = "";
public string OriginalPath { get; set; } = ""; public string OriginalPath { get; set; } = "";
public ChangeDataForAmend DataForAmend { get; set; } = null; public ChangeDataForAmend DataForAmend { get; set; } = null;
public ConflictReason ConflictReason { get; set; } = ConflictReason.None;
public bool IsConflicted => WorkTree == ChangeState.Conflicted; public bool IsConflit
public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason]; {
public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason]; get
{
public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree]; if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged)
public string IndexDesc => TYPE_DESCS[(int)Index]; return true;
if (Index == ChangeState.Added && WorkTree == ChangeState.Added)
return true;
if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted)
return true;
return false;
}
}
public void Set(ChangeState index, ChangeState workTree = ChangeState.None) public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{ {
@ -83,44 +76,8 @@ namespace SourceGit.Models
if (Path[0] == '"') if (Path[0] == '"')
Path = Path.Substring(1, Path.Length - 2); Path = Path.Substring(1, Path.Length - 2);
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"')
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2); OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
} }
private static readonly string[] TYPE_DESCS =
[
"Unknown",
"Modified",
"Type Changed",
"Added",
"Deleted",
"Renamed",
"Copied",
"Untracked",
"Conflict"
];
private static readonly string[] CONFLICT_MARKERS =
[
string.Empty,
"DD",
"AU",
"UD",
"UA",
"DU",
"AA",
"UU"
];
private static readonly string[] CONFLICT_DESCS =
[
string.Empty,
"Both deleted",
"Added by us",
"Deleted by them",
"Added by them",
"Deleted by us",
"Both added",
"Both modified"
];
} }
} }

View file

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

View file

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

View file

@ -1,49 +1,8 @@
using System; namespace SourceGit.Models
using System.Collections.Generic;
namespace SourceGit.Models
{ {
public class CommitLink public class CommitLink
{ {
public string Name { get; set; } = null; public string Name { get; set; } = null;
public string URLPrefix { get; set; } = null; public string URLPrefix { get; set; } = null;
public CommitLink(string name, string prefix)
{
Name = name;
URLPrefix = prefix;
}
public static List<CommitLink> Get(List<Remote> remotes)
{
var outs = new List<CommitLink>();
foreach (var remote in remotes)
{
if (remote.TryGetVisitURL(out var url))
{
var trimmedUrl = url.AsSpan();
if (url.EndsWith(".git"))
trimmedUrl = url.AsSpan(0, url.Length - 4);
if (url.StartsWith("https://github.com/", StringComparison.Ordinal))
outs.Add(new($"Github ({trimmedUrl.Slice(19)})", $"{url}/commit/"));
else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal))
outs.Add(new($"GitLab ({trimmedUrl.Slice(trimmedUrl.Slice(15).IndexOf('/') + 16)})", $"{url}/-/commit/"));
else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal))
outs.Add(new($"Gitee ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal))
outs.Add(new($"BitBucket ({trimmedUrl.Slice(22)})", $"{url}/commits/"));
else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal))
outs.Add(new($"Codeberg ({trimmedUrl.Slice(21)})", $"{url}/commit/"));
else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal))
outs.Add(new($"Gitea ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal))
outs.Add(new($"sourcehut ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
}
}
return outs;
}
} }
} }

View file

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

View file

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

View file

@ -1,19 +0,0 @@
using System;
namespace SourceGit.Models
{
public class Count : IDisposable
{
public int Value { get; set; } = 0;
public Count(int value)
{
Value = value;
}
public void Dispose()
{
// Ignore
}
}
}

View file

@ -25,24 +25,24 @@ namespace SourceGit.Models
set; set;
} = 0; } = 0;
public static DateTimeFormat Active public static DateTimeFormat Actived
{ {
get => Supported[ActiveIndex]; get => Supported[ActiveIndex];
} }
public static readonly List<DateTimeFormat> Supported = new List<DateTimeFormat> 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("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("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("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("MMM d yyyy", "MMM d yyyy HH:mm:ss"),
new DateTimeFormat("d MMM yyyy", "d MMM 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); private static readonly DateTime _example = new DateTime(2025, 1, 31, 8, 0, 0, DateTimeKind.Local);

View file

@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace SourceGit.Models
{
public class DealWithChangesAfterStashing
{
public string Label { get; set; }
public string Desc { get; set; }
public static readonly List<DealWithChangesAfterStashing> Supported = [
new ("Discard", "All (or selected) changes will be discarded"),
new ("Keep Index", "Staged changes are left intact"),
new ("Keep All", "All (or selected) changes are left intact"),
];
public DealWithChangesAfterStashing(string label, string desc)
{
Label = label;
Desc = desc;
}
}
}

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