Merge commit '55be1ad1ca' into add_locale_ja_JP

This commit is contained in:
Sousi Omine 2025-04-02 00:37:03 +09:00
commit 50804fdbc7
288 changed files with 10124 additions and 6428 deletions

View file

@ -19,14 +19,25 @@ jobs:
os: macos-latest
runtime: osx-arm64
- name : Linux
os: ubuntu-20.04
os: ubuntu-latest
runtime: linux-x64
container: ubuntu:20.04
- name : Linux (arm64)
os: ubuntu-20.04
os: ubuntu-latest
runtime: linux-arm64
container: ubuntu:20.04
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
container: ${{ matrix.container || '' }}
steps:
- name: Install common CLI tools
if: ${{ startsWith(matrix.runtime, 'linux-') }}
run: |
export DEBIAN_FRONTEND=noninteractive
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
apt-get update
apt-get install -y sudo
sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup .NET
@ -47,7 +58,7 @@ jobs:
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
sudo apt-get update
sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
sudo apt-get install -y llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
- name: Build
run: dotnet build -c Release
- name: Publish

View file

@ -7,12 +7,12 @@ on:
required: true
type: string
jobs:
windows-portable:
name: Package portable Windows app
runs-on: ubuntu-latest
windows:
name: Package Windows
runs-on: windows-2019
strategy:
matrix:
runtime: [win-x64, win-arm64]
runtime: [ win-x64, win-arm64 ]
steps:
- name: Checkout sources
uses: actions/checkout@v4
@ -22,10 +22,11 @@ jobs:
name: sourcegit.${{ matrix.runtime }}
path: build/SourceGit
- name: Package
shell: bash
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
run: ./build/scripts/package.windows-portable.sh
run: ./build/scripts/package.windows.sh
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
@ -36,7 +37,7 @@ jobs:
with:
name: sourcegit.${{ matrix.runtime }}
osx-app:
name: Package OSX app
name: Package macOS
runs-on: macos-latest
strategy:
matrix:
@ -69,6 +70,7 @@ jobs:
linux:
name: Package Linux
runs-on: ubuntu-latest
container: ubuntu:20.04
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
@ -77,9 +79,10 @@ jobs:
uses: actions/checkout@v4
- name: Download package dependencies
run: |
sudo add-apt-repository universe
sudo apt-get update
sudo apt-get install desktop-file-utils rpm libfuse2
export DEBIAN_FRONTEND=noninteractive
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
apt-get update
apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils
- name: Download build
uses: actions/download-artifact@v4
with:
@ -89,6 +92,7 @@ jobs:
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
APPIMAGE_EXTRACT_AND_RUN: 1
run: |
mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit

View file

@ -1,39 +0,0 @@
name: Publish to Buildkite
on:
workflow_call:
secrets:
BUILDKITE_TOKEN:
required: true
jobs:
publish:
name: Publish to Buildkite
runs-on: ubuntu-latest
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
steps:
- name: Download package artifacts
uses: actions/download-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: packages
- name: Publish DEB package
env:
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
run: |
FILE=$(echo packages/*.deb)
curl -X POST https://api.buildkite.com/v2/packages/organizations/sourcegit/registries/sourcegit-deb/packages \
-H "Authorization: Bearer $BUILDKITE_TOKEN" \
-F "file=@$FILE" \
--fail
- name: Publish RPM package
env:
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
run: |
FILE=$(echo packages/*.rpm)
curl -X POST https://api.buildkite.com/v2/packages/organizations/sourcegit/registries/sourcegit-rpm/packages \
-H "Authorization: Bearer $BUILDKITE_TOKEN" \
-F "file=@$FILE" \
--fail

View file

@ -24,12 +24,6 @@ jobs:
uses: ./.github/workflows/package.yml
with:
version: ${{ needs.version.outputs.version }}
publish-packages:
needs: [package, version]
name: Publish Packages
uses: ./.github/workflows/publish-packages.yml
secrets:
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
release:
needs: [package, version]
name: Release
@ -44,7 +38,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.version.outputs.version }}
run: gh release create "$TAG" -t "Release $VERSION" --notes-from-tag
run: gh release create "$TAG" -t "$VERSION" --notes-from-tag
- name: Download artifacts
uses: actions/download-artifact@v4
with:

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2024 sourcegit
Copyright (c) 2025 sourcegit
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -18,9 +18,9 @@
* Supports SSH access with each remote
* GIT commands with GUI
* Clone/Fetch/Pull/Push...
* Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
* Amend/Reword
* Interactive rebase (Basic)
* Merge/Rebase/Reset/Revert/Cherry-pick...
* Amend/Reword/Squash
* Interactive rebase
* Branches
* Remotes
* Tags
@ -40,6 +40,7 @@
* Git LFS
* Issue Link
* Workspace
* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING]
@ -47,7 +48,7 @@
## Translation Status
[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-97.50%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-97.78%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-95.00%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-95.56%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-96.81%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-97.92%25-yellow)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md)
You can find the current translation status in [TRANSLATION.md](TRANSLATION.md)
## How to Use
@ -59,12 +60,13 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD
| OS | PATH |
|---------|-----------------------------------------------------|
| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` |
| Windows | `%APPDATA%\SourceGit` |
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
| macOS | `${HOME}/Library/Application Support/SourceGit` |
> [!TIP]
> You can open the app data dir from the main menu.
> * You can open this data storage directory from the main menu `Open Data Storage Directory`.
> * You can create a `data` folder next to the `SourceGit` executable to force this app to store data (user settings, downloaded avatars and crash logs) into it (Portable-Mode). Only works on Windows.
For **Windows** users:
@ -75,12 +77,12 @@ For **Windows** users:
```
> [!NOTE]
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
* You can install the latest stable by `scoope` with follow commands:
* You can install the latest stable by `scoop` with follow commands:
```shell
scoop bucket add extras
scoop install sourcegit
```
* Portable versions can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest)
* Pre-built binaries can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest)
For **macOS** users:
@ -98,49 +100,45 @@ For **macOS** users:
For **Linux** users:
* For Debian/Ubuntu based distributions, you can add the `sourcegit` repository by following:
You may need to install curl and/or gpg first, if you're on a very minimal host:
* Thanks [@aikawayataro](https://github.com/aikawayataro) for providing `rpm` and `deb` repositories, hosted on [Codeberg](https://codeberg.org/yataro/-/packages).
`deb` how to:
```shell
apt update && apt install curl gpg -y
curl https://codeberg.org/api/packages/yataro/debian/repository.key | sudo tee /etc/apt/keyrings/sourcegit.asc
echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc, arch=amd64,arm64] https://codeberg.org/api/packages/yataro/debian generic main" | sudo tee /etc/apt/sources.list.d/sourcegit.list
sudo apt update
sudo apt install sourcegit
```
Install the registry signing key:
`rpm` how to:
```shell
curl -fsSL "https://packages.buildkite.com/sourcegit/sourcegit-deb/gpgkey" | gpg --dearmor -o /etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg
curl https://codeberg.org/api/packages/yataro/rpm.repo | sed -e 's/gpgcheck=1/gpgcheck=0/' > sourcegit.repo
# Fedora 41 and newer
sudo dnf config-manager addrepo --from-repofile=./sourcegit.repo
# Fedora 40 and earlier
sudo dnf config-manager --add-repo ./sourcegit.repo
sudo dnf install sourcegit
```
Configure the source:
```shell
echo -e "deb [signed-by=/etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg] https://packages.buildkite.com/sourcegit/sourcegit-deb/any/ any main\ndeb-src [signed-by=/etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg] https://packages.buildkite.com/sourcegit/sourcegit-deb/any/ any main" > /etc/apt/sources.list.d/buildkite-sourcegit-sourcegit-deb.list
```
Update your local repository and install the package:
```shell
apt update && apt install sourcegit
```
* For RHEL/Fedora based distributions, you can add the `sourcegit` repository by following:
Configure the source:
```shell
sudo sh -c 'echo -e "[sourcegit-rpm]\nname=sourcegit-rpm\nbaseurl=https://packages.buildkite.com/sourcegit/sourcegit-rpm/rpm_any/rpm_any/\$basearch\nenabled=1\nrepo_gpgcheck=1\ngpgcheck=0\ngpgkey=https://packages.buildkite.com/sourcegit/sourcegit-rpm/gpgkey\npriority=1"' > /etc/yum.repos.d/sourcegit-rpm.repo
```
Install the package with this command:
```shell
sudo dnf install -y sourcegit
```
* `Appimage` files can be found on [AppimageHub](https://appimage.github.io/SourceGit/)
* `xdg-open` must be installed to support open native file manager.
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux.
If your distribution isn't using `dnf`, please refer to the documentation of your distribution on how to add an `rpm` repository.
* `AppImage` files can be found on [AppImage hub](https://appimage.github.io/SourceGit/), `xdg-open` (`xdg-utils`) must be installed to support open native file manager.
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your Linux.
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
* If you can NOT type accented characters, such as `ê`, `ó`, try to set the environment variable `AVALONIA_IM_MODULE` to `none`.
* If you can NOT type accented characters, such as `ê`, `ó`, try to set the environment variable `AVALONIA_IM_MODULE` to `none`.
## OpenAI
This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window.
This software supports using OpenAI or other AI service that has an OpenAI compatible HTTP API to generate commit message. You need configurate the service in `Preference` window.
For `OpenAI`:
* `Server` must be `https://api.openai.com/v1/chat/completions`
* `Server` must be `https://api.openai.com/v1`
For other AI service:
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate`
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate`
* The `API Key` is optional that depends on the service
## External Tools
@ -159,7 +157,7 @@ This app supports open repository in external tools listed in the table below.
> [!NOTE]
> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app.
> To solve this problem you can add a file named `external_editors.json` in app data dir and provide the path directly. For example:
> To solve this problem you can add a file named `external_editors.json` in app data storage directory and provide the path directly. For example:
```json
{
"tools": {
@ -189,6 +187,19 @@ This app supports open repository in external tools listed in the table below.
Everyone is welcome to submit a PR. Please make sure your PR is based on the latest `develop` branch and the target branch of PR is `develop`.
In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download):
```sh
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
dotnet restore
dotnet build
dotnet run --project src/SourceGit.csproj
```
Thanks to all the people who contribute.
[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=20)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
## Third-Party Components
For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md).

View file

@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\package.yml = .github\workflows\package.yml
.github\workflows\release.yml = .github\workflows\release.yml
.github\workflows\localization-check.yml = .github\workflows\localization-check.yml
.github\workflows\publish-packages.yml = .github\workflows\publish-packages.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}"
@ -61,6 +60,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}"
ProjectSection(SolutionItems) = preProject
build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control
build\resources\deb\DEBIAN\preinst = build\resources\deb\DEBIAN\preinst
build\resources\deb\DEBIAN\prerm = build\resources\deb\DEBIAN\prerm
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}"
@ -81,7 +82,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C54D
build\scripts\localization-check.js = build\scripts\localization-check.js
build\scripts\package.linux.sh = build\scripts\package.linux.sh
build\scripts\package.osx-app.sh = build\scripts\package.osx-app.sh
build\scripts\package.windows-portable.sh = build\scripts\package.windows-portable.sh
build\scripts\package.windows.sh = build\scripts\package.windows.sh
EndProjectSection
EndProject
Global

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

@ -0,0 +1,86 @@
# Third-Party Licenses
This project incorporates components from the following third parties:
## Packages
### AvaloniaUI
- **Source**: https://github.com/AvaloniaUI/Avalonia
- **Version**: 11.2.5
- **License**: MIT License
- **License Link**: https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md
### AvaloniaEdit
- **Source**: https://github.com/AvaloniaUI/AvaloniaEdit
- **Version**: 11.2.0
- **License**: MIT License
- **License Link**: https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/LICENSE
### LiveChartsCore.SkiaSharpView.Avalonia
- **Source**: https://github.com/beto-rodriguez/LiveCharts2
- **Version**: 2.0.0-rc5.4
- **License**: MIT License
- **License Link**: https://github.com/beto-rodriguez/LiveCharts2/blob/master/LICENSE
### TextMateSharp
- **Source**: https://github.com/danipen/TextMateSharp
- **Version**: 1.0.66
- **License**: MIT License
- **License Link**: https://github.com/danipen/TextMateSharp/blob/master/LICENSE.md
### OpenAI .NET SDK
- **Source**: https://github.com/openai/openai-dotnet
- **Version**: 2.2.0-beta2
- **License**: MIT License
- **License Link**: https://github.com/openai/openai-dotnet/blob/main/LICENSE
### Azure.AI.OpenAI
- **Source**: https://github.com/Azure/azure-sdk-for-net
- **Version**: 2.2.0-beta2
- **License**: MIT License
- **License Link**: https://github.com/Azure/azure-sdk-for-net/blob/main/LICENSE.txt
## Fonts
### JetBrainsMono
- **Source**: https://github.com/JetBrains/JetBrainsMono
- **Commit**: v2.304
- **License**: SIL Open Font License, Version 1.1
- **License Link**: https://github.com/JetBrains/JetBrainsMono/blob/v2.304/OFL.txt
## Grammar Files
### haxe-TmLanguage
- **Source**: https://github.com/vshaxe/haxe-TmLanguage
- **Commit**: ddad8b4c6d0781ac20be0481174ec1be772c5da5
- **License**: MIT License
- **License Link**: https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md
### coc-toml
- **Source**: https://github.com/kkiyama117/coc-toml
- **Commit**: aac3e0c65955c03314b2733041b19f903b7cc447
- **License**: MIT License
- **License Link**: https://github.com/kkiyama117/coc-toml/blob/aac3e0c65955c03314b2733041b19f903b7cc447/LICENSE
### eclipse-buildship
- **Source**: https://github.com/eclipse/buildship
- **Commit**: 6bb773e7692f913dec27105129ebe388de34e68b
- **License**: Eclipse Public License 1.0
- **License Link**: https://github.com/eclipse-buildship/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/README.md
### vscode-jsp-lang
- **Source**: https://github.com/samuel-weinhardt/vscode-jsp-lang
- **Commit**: 0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355
- **License**: MIT License
- **License Link**: https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/LICENSE

View file

@ -1,117 +1,91 @@
### de_DE.axaml: 97.50%
# Translation Status
This document shows the translation status of each locale file in the repository.
## Details
### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)
### ![de__DE](https://img.shields.io/badge/de__DE-98.65%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in de_DE.axaml</summary>
- Text.BranchCM.MergeMultiBranches
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitDetail.Files.Search
- Text.Diff.UseBlockNavigation
- 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.Repository.Skip
- Text.WorkingCopy.CommitToEdit
- Text.BranchUpstreamInvalid
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.IssueTracker.AddSampleAzure
- Text.CopyFullPath
- Text.Diff.First
- Text.Diff.Last
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.ShowTagsInGraph
- Text.StashCM.SaveAsPatch
</details>
### es_ES.axaml: 97.78%
### ![es__ES](https://img.shields.io/badge/es__ES-99.87%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in es_ES.axaml</summary>
- Text.BranchCM.MergeMultiBranches
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.Diff.UseBlockNavigation
- 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.Repository.Skip
- Text.CopyFullPath
</details>
### fr_FR.axaml: 95.00%
### ![fr__FR](https://img.shields.io/badge/fr__FR-99.87%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in fr_FR.axaml</summary>
- Text.BranchCM.MergeMultiBranches
- Text.CherryPick.AppendSourceToMessage
- Text.CherryPick.Mainline.Tips
- Text.CommitCM.CherryPickMultiple
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitDetail.Files.Search
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.Hotkeys.Global.Clone
- Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head
- Text.Merge.Source
- Text.MergeMultiple
- Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets
- Text.Preference.Appearance.FontSize
- Text.Preference.Appearance.FontSize.Default
- Text.Preference.Appearance.FontSize.Editor
- Text.Preference.General.ShowChildren
- Text.Repository.CustomActions
- Text.Repository.FilterCommits
- Text.Repository.FilterCommits.Default
- Text.Repository.FilterCommits.Exclude
- Text.Repository.FilterCommits.Include
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
- Text.Repository.Skip
- Text.ScanRepositories
- Text.SHALinkCM.NavigateTo
- Text.WorkingCopy.CommitToEdit
- Text.CopyFullPath
</details>
### it_IT.axaml: 95.56%
### ![it__IT](https://img.shields.io/badge/it__IT-99.73%25-yellow)
<details>
<summary>Missing Keys</summary>
<summary>Missing keys in it_IT.axaml</summary>
- Text.CopyFullPath
- Text.Preferences.General.ShowTagsInGraph
</details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-90.98%25-yellow)
<details>
<summary>Missing keys in pt_BR.axaml</summary>
- Text.AIAssistant.Regen
- Text.AIAssistant.Use
- Text.ApplyStash
- Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash
- Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
- Text.BranchUpstreamInvalid
- Text.Clone.RecurseSubmodules
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
- Text.Configure.OpenAI.Preferred
- Text.Configure.OpenAI.Preferred.Tip
- Text.Configure.CustomAction.Scope.Branch
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.IssueTracker.AddSampleGiteeIssue
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
- Text.CopyFullPath
- Text.CreateBranch.Name.WarnSpace
- Text.DeleteRepositoryNode.Path
- Text.DeleteRepositoryNode.TipForGroup
- Text.DeleteRepositoryNode.TipForRepository
- Text.Diff.First
- Text.Diff.Last
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.Hotkeys.Global.Clone
- Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt
@ -121,93 +95,40 @@
- Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets
- Text.Preference.General.ShowChildren
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph
- Text.Preferences.Git.SSLVerify
- Text.Repository.FilterCommits
- Text.Repository.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.OnlyHighlightCurrentBranchInHistories
- Text.Repository.Skip
- Text.SHALinkCM.CopySHA
- Text.Repository.Tags.OrderByCreatorDate
- Text.Repository.Tags.OrderByNameAsc
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories
- Text.SetUpstream
- Text.SetUpstream.Local
- Text.SetUpstream.Unset
- Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo
- Text.Stash.AutoRestore
- Text.Stash.AutoRestore.Tip
- Text.StashCM.SaveAsPatch
- Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.SignOff
</details>
### pt_BR.axaml: 96.81%
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)
### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)
<details>
<summary>Missing Keys</summary>
- Text.BranchCM.MergeMultiBranches
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.Hotkeys.Global.Clone
- Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head
- Text.Merge.Source
- Text.MergeMultiple
- Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets
- Text.Preference.General.ShowChildren
- Text.Repository.FilterCommits
- Text.Repository.Skip
- Text.SHALinkCM.NavigateTo
- Text.WorkingCopy.CommitToEdit
</details>
### ru_RU.axaml: 97.92%
<details>
<summary>Missing Keys</summary>
- Text.BranchCM.MergeMultiBranches
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- 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.Repository.Skip
</details>
### zh_CN.axaml: 100.00%
<details>
<summary>Missing Keys</summary>
</details>
### zh_TW.axaml: 100.00%
<details>
<summary>Missing Keys</summary>
</details>
### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)

View file

@ -1 +1 @@
8.42
2025.11

View file

@ -1,7 +1,8 @@
Package: sourcegit
Version: 8.23
Version: 2025.10
Priority: optional
Depends: libx11-6, libice6, libsm6
Depends: libx11-6, libice6, libsm6, libicu | libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu69 | libicu68 | libicu67 | libicu66 | libicu65 | libicu63 | libicu60 | libicu57 | libicu55 | libicu52, xdg-utils
Architecture: amd64
Installed-Size: 60440
Maintainer: longshuang@msn.cn
Description: Open-source & Free Git GUI Client

View file

@ -0,0 +1,32 @@
#!/bin/sh
set -e
# summary of how this script can be called:
# * <new-preinst> `install'
# * <new-preinst> `install' <old-version>
# * <new-preinst> `upgrade' <old-version>
# * <old-preinst> `abort-upgrade' <new-version>
# for details, see http://www.debian.org/doc/debian-policy/
case "$1" in
install|upgrade)
# Check if SourceGit is running and stop it
if pgrep -f '/opt/sourcegit/sourcegit' > /dev/null; then
echo "Stopping running SourceGit instance..."
pkill -f '/opt/sourcegit/sourcegit' || true
# Give the process a moment to terminate
sleep 1
fi
;;
abort-upgrade)
;;
*)
echo "preinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View file

@ -0,0 +1,35 @@
#!/bin/sh
set -e
# summary of how this script can be called:
# * <prerm> `remove'
# * <old-prerm> `upgrade' <new-version>
# * <new-prerm> `failed-upgrade' <old-version>
# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
# * <deconfigured's-prerm> `deconfigure' `in-favour'
# <package-being-installed> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
remove|upgrade|deconfigure)
if pgrep -f '/opt/sourcegit/sourcegit' > /dev/null; then
echo "Stopping running SourceGit instance..."
pkill -f '/opt/sourcegit/sourcegit' || true
# Give the process a moment to terminate
sleep 1
fi
;;
failed-upgrade)
;;
*)
echo "prerm called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View file

@ -8,6 +8,7 @@ Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.
Requires: libX11.so.6()(%{__isa_bits}bit)
Requires: libSM.so.6()(%{__isa_bits}bit)
Requires: libicu
Requires: xdg-utils
%define _build_id_links none

View file

@ -6,7 +6,6 @@ const repoRoot = path.join(__dirname, '../../');
const localesDir = path.join(repoRoot, 'src/Resources/Locales');
const enUSFile = path.join(localesDir, 'en_US.axaml');
const outputFile = path.join(repoRoot, 'TRANSLATION.md');
const readmeFile = path.join(repoRoot, 'README.md');
const parser = new xml2js.Parser();
@ -18,42 +17,36 @@ async function parseXml(filePath) {
async function calculateTranslationRate() {
const enUSData = await parseXml(enUSFile);
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
const translationRates = [];
const badges = [];
const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml'));
// Add en_US badge first
badges.push(`[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md)`);
const lines = [];
lines.push('# Translation Status');
lines.push('This document shows the translation status of each locale file in the repository.');
lines.push(`## Details`);
lines.push(`### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)`);
for (const file of files) {
const locale = file.replace('.axaml', '').replace('_', '__');
const filePath = path.join(localesDir, file);
const localeData = await parseXml(filePath);
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
const translationRate = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
translationRates.push(`### ${file}: ${translationRate.toFixed(2)}%\n`);
translationRates.push(`<details>\n<summary>Missing Keys</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`);
if (missingKeys.length > 0) {
const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
const badgeColor = progress >= 75 ? 'yellow' : 'red';
// Add badges
const locale = file.replace('.axaml', '').replace('_', '__');
const badgeColor = translationRate === 100 ? 'brightgreen' : translationRate >= 75 ? 'yellow' : 'red';
badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-${translationRate.toFixed(2)}%25-${badgeColor})](TRANSLATION.md)`);
lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-${progress.toFixed(2)}%25-${badgeColor})`);
lines.push(`<details>\n<summary>Missing keys in ${file}</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`)
} else {
lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)`);
}
}
console.log(translationRates.join('\n\n'));
await fs.writeFile(outputFile, translationRates.join('\n\n') + '\n', 'utf8');
// Update README.md
let readmeContent = await fs.readFile(readmeFile, 'utf8');
const badgeSection = `## Translation Status\n\n${badges.join(' ')}`;
console.log(badgeSection);
readmeContent = readmeContent.replace(/## Translation Status\n\n.*\n\n/, badgeSection + '\n\n');
await fs.writeFile(readmeFile, readmeContent, 'utf8');
const content = lines.join('\n\n');
console.log(content);
await fs.writeFile(outputFile, content, 'utf8');
}
calculateTranslationRate().catch(err => console.error(err));

View file

@ -56,8 +56,15 @@ cp -f SourceGit/* resources/deb/opt/sourcegit
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
cp -r resources/_common/applications resources/deb/usr/share
cp -r resources/_common/icons resources/deb/usr/share
sed -i -e "s/^Version:.*/Version: $VERSION/" -e "s/^Architecture:.*/Architecture: $arch/" resources/deb/DEBIAN/control
dpkg-deb --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
# Calculate installed size in KB
installed_size=$(du -sk resources/deb | cut -f1)
# Update the control file
sed -i -e "s/^Version:.*/Version: $VERSION/" \
-e "s/^Architecture:.*/Architecture: $arch/" \
-e "s/^Installed-Size:.*/Installed-Size: $installed_size/" \
resources/deb/DEBIAN/control
# Build deb package with gzip compression
dpkg-deb -Zgzip --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./

View file

@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -e
set -o
set -u
set pipefail
cd build
rm -rf SourceGit/*.pdb
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -e
set -o
set -u
set pipefail
cd build
rm -rf SourceGit/*.pdb
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
powershell -Command "Compress-Archive -Path SourceGit -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force"
else
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
fi

View file

@ -25,12 +25,34 @@ namespace SourceGit
private Action<object> _action = null;
}
public static readonly Command OpenPreferenceCommand = new Command(_ => OpenDialog(new Views.Preference()));
public static bool IsCheckForUpdateCommandVisible
{
get
{
#if DISABLE_UPDATE_DETECTION
return false;
#else
return true;
#endif
}
}
public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
public static readonly Command CheckForUpdateCommand = new Command(_ => 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 CopyTextBlockCommand = new Command(p => CopyTextBlock(p as TextBlock));
public static readonly Command CopyTextBlockCommand = new Command(p =>
{
var textBlock = p as TextBlock;
if (textBlock == null)
return;
if (textBlock.Inlines is { Count: > 0 } inlines)
CopyText(inlines.Text);
else if (!string.IsNullOrEmpty(textBlock.Text))
CopyText(textBlock.Text);
});
}
}

View file

@ -46,11 +46,9 @@ namespace SourceGit
[JsonSerializable(typeof(Models.ExternalToolPaths))]
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
[JsonSerializable(typeof(Models.JetBrainsState))]
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
[JsonSerializable(typeof(Models.ThemeOverrides))]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.RepositorySettings))]
[JsonSerializable(typeof(ViewModels.Preference))]
[JsonSerializable(typeof(ViewModels.Preferences))]
internal partial class JsonCodeGen : JsonSerializerContext { }
}

View file

@ -34,9 +34,9 @@
<NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preference}" Command="{x:Static s:App.OpenPreferenceCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>

View file

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
@ -22,6 +24,7 @@ namespace SourceGit
{
public partial class App : Application
{
#region App Entry Point
[STAThread]
public static void Main(string[] args)
{
@ -34,15 +37,14 @@ namespace SourceGit
TaskScheduler.UnobservedTaskException += (_, e) =>
{
LogException(e.Exception);
e.SetObserved();
};
try
{
if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo))
if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo))
Environment.Exit(exitTodo);
else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage))
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
Environment.Exit(exitMessage);
else
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
@ -75,34 +77,33 @@ namespace SourceGit
return builder;
}
public override void Initialize()
private static void LogException(Exception ex)
{
AvaloniaXamlLoader.Load(this);
if (ex == null)
return;
var pref = ViewModels.Preference.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
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);
SetLocale(pref.Locale);
SetTheme(pref.Theme, pref.ThemeOverrides);
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
BindingPlugins.DataValidators.RemoveAt(0);
if (TryLaunchedAsCoreEditor(desktop))
return;
if (TryLaunchedAsAskpass(desktop))
return;
TryLaunchedAsNormal(desktop);
}
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
File.WriteAllText(file, builder.ToString());
}
#endregion
#region Utility Functions
public static void OpenDialog(Window window)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
@ -204,6 +205,9 @@ namespace SourceGit
app._fontsOverrides = null;
}
defaultFont = app.FixFontFamilyName(defaultFont);
monospaceFont = app.FixFontFamilyName(monospaceFont);
var resDic = new ResourceDictionary();
if (!string.IsNullOrEmpty(defaultFont))
resDic.Add("Fonts.Default", new FontFamily(defaultFont));
@ -304,21 +308,6 @@ namespace SourceGit
return Current is App app ? app._launcher : null;
}
public static ViewModels.Repository FindOpenedRepository(string repoPath)
{
if (Current is App app && app._launcher != null)
{
foreach (var page in app._launcher.Pages)
{
var id = page.Node.Id.Replace("\\", "/");
if (id == repoPath && page.Data is ViewModels.Repository repo)
return repo;
}
}
return null;
}
public static void Quit(int exitCode)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@ -331,94 +320,39 @@ namespace SourceGit
Environment.Exit(exitCode);
}
}
#endregion
private static void CopyTextBlock(TextBlock textBlock)
#region Overrides
public override void Initialize()
{
if (textBlock == null)
return;
AvaloniaXamlLoader.Load(this);
if (textBlock.Inlines is { Count: > 0 } inlines)
CopyText(inlines.Text);
else if (!string.IsNullOrEmpty(textBlock.Text))
CopyText(textBlock.Text);
var pref = ViewModels.Preferences.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
SetLocale(pref.Locale);
SetTheme(pref.Theme, pref.ThemeOverrides);
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
}
private static void LogException(Exception ex)
public override void OnFrameworkInitializationCompleted()
{
if (ex == null)
return;
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion.ToString()}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex.StackTrace);
while (ex.InnerException != null)
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
ex = ex.InnerException;
builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n");
builder.Append(ex.StackTrace);
BindingPlugins.DataValidators.RemoveAt(0);
if (TryLaunchAsCoreEditor(desktop))
return;
if (TryLaunchAsAskpass(desktop))
return;
TryLaunchAsNormal(desktop);
}
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
private static void Check4Update(bool manually = false)
{
Task.Run(async () =>
{
try
{
// Fetch lastest release information.
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) };
var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json");
// Parse json into Models.Version.
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
if (ver == null)
return;
// Check if already up-to-date.
if (!ver.IsNewVersion)
{
if (manually)
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
return;
}
// Should not check ignored tag if this is called manually.
if (!manually)
{
var pref = ViewModels.Preference.Instance;
if (ver.TagName == pref.IgnoreUpdateTag)
return;
}
ShowSelfUpdateResult(ver);
}
catch (Exception e)
{
if (manually)
ShowSelfUpdateResult(e);
}
});
}
private static void ShowSelfUpdateResult(object data)
{
Dispatcher.UIThread.Post(() =>
{
OpenDialog(new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } });
});
}
private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode)
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -471,7 +405,7 @@ namespace SourceGit
return true;
}
private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode)
private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -505,7 +439,7 @@ namespace SourceGit
return true;
}
private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{
var args = desktop.Args;
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
@ -513,14 +447,18 @@ namespace SourceGit
var file = args[1];
if (!File.Exists(file))
{
desktop.Shutdown(-1);
else
desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
return true;
}
var editor = new Views.StandaloneCommitMessageEditor();
editor.SetFile(file);
desktop.MainWindow = editor;
return true;
}
private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
{
var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS");
if (launchAsAskpass is not "TRUE")
@ -529,14 +467,16 @@ namespace SourceGit
var args = desktop.Args;
if (args?.Length > 0)
{
desktop.MainWindow = new Views.Askpass(args[0]);
var askpass = new Views.Askpass();
askpass.TxtDescription.Text = args[0];
desktop.MainWindow = askpass;
return true;
}
return false;
}
private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{
Native.OS.SetupEnternalTools();
Models.AvatarManager.Instance.Start();
@ -548,9 +488,96 @@ namespace SourceGit
_launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
var pref = ViewModels.Preference.Instance;
#if !DISABLE_UPDATE_DETECTION
var pref = ViewModels.Preferences.Instance;
if (pref.ShouldCheck4UpdateOnStartup())
Check4Update();
#endif
}
private void Check4Update(bool manually = false)
{
Task.Run(async () =>
{
try
{
// Fetch latest release information.
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) };
var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json");
// Parse JSON into Models.Version.
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
if (ver == null)
return;
// Check if already up-to-date.
if (!ver.IsNewVersion)
{
if (manually)
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
return;
}
// Should not check ignored tag if this is called manually.
if (!manually)
{
var pref = ViewModels.Preferences.Instance;
if (ver.TagName == pref.IgnoreUpdateTag)
return;
}
ShowSelfUpdateResult(ver);
}
catch (Exception e)
{
if (manually)
ShowSelfUpdateResult(new Models.SelfUpdateFailed(e));
}
});
}
private void ShowSelfUpdateResult(object data)
{
Dispatcher.UIThread.Post(() =>
{
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;
}
trimmed.Add(sb.ToString());
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
private ViewModels.Launcher _launcher = null;

View file

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

View file

@ -65,7 +65,7 @@ namespace SourceGit.Commands
var commit = match.Groups[1].Value;
var author = match.Groups[2].Value;
var timestamp = int.Parse(match.Groups[3].Value);
var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd");
var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString(_dateFormat);
var info = new Models.BlameLineInfo()
{
@ -87,6 +87,7 @@ namespace SourceGit.Commands
private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder();
private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;

View file

@ -54,21 +54,14 @@
public static bool DeleteRemote(string repo, string remote, string name)
{
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
return new Push(repo, remote, $"refs/heads/{name}", true).Exec();
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
{
cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
cmd.Args = $"push {remote} --delete {name}";
}
else
{
cmd.Args = $"branch -D -r {remote}/{name}";
}
cmd.Args = $"branch -D -r {remote}/{name}";
return cmd.Exec();
}
}

View file

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

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Avalonia.Threading;
@ -10,11 +11,6 @@ namespace SourceGit.Commands
{
public partial class Command
{
public class CancelToken
{
public bool Requested { get; set; } = false;
}
public class ReadToEndResult
{
public bool IsSuccess { get; set; } = false;
@ -30,7 +26,7 @@ namespace SourceGit.Commands
}
public string Context { get; set; } = string.Empty;
public CancelToken Cancel { get; set; } = null;
public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
public string WorkingDirectory { get; set; } = null;
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
public string SSHKey { get; set; } = string.Empty;
@ -43,36 +39,15 @@ namespace SourceGit.Commands
var start = CreateGitStartInfo();
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (string.IsNullOrEmpty(e.Data))
{
errs.Add(string.Empty);
@ -97,9 +72,25 @@ namespace SourceGit.Commands
errs.Add(e.Data);
};
var dummy = null as Process;
var dummyProcLock = new object();
try
{
proc.Start();
// It not safe, please only use `CancellationToken` in readonly commands.
if (CancellationToken.CanBeCanceled)
{
dummy = proc;
CancellationToken.Register(() =>
{
lock (dummyProcLock)
{
if (dummy is { HasExited: false })
dummy.Kill();
}
});
}
}
catch (Exception e)
{
@ -113,15 +104,23 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine();
proc.WaitForExit();
if (dummy != null)
{
lock (dummyProcLock)
{
dummy = null;
}
}
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0)
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
if (RaiseError)
{
var errMsg = string.Join("\n", errs);
if (!string.IsNullOrWhiteSpace(errMsg))
var errMsg = string.Join("\n", errs).Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg));
}
@ -192,13 +191,12 @@ namespace SourceGit.Commands
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
// Force using en_US.UTF-8 locale to avoid GCM crash
// Force using en_US.UTF-8 locale
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
{
start.Environment.Add("LANG", "C");
start.Environment.Add("LC_ALL", "C");
}
// Force using this app as git editor.
switch (Editor)

View file

@ -6,7 +6,7 @@ namespace SourceGit.Commands
{
public partial class CompareRevisions : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
[GeneratedRegex(@"^([MADRC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
public CompareRevisions(string repo, string start, string end)
@ -18,6 +18,15 @@ namespace SourceGit.Commands
Args = $"diff --name-status {based} {end}";
}
public CompareRevisions(string repo, string start, string end, string path)
{
WorkingDirectory = repo;
Context = repo;
var based = string.IsNullOrEmpty(start) ? "-R" : start;
Args = $"diff --name-status {based} {end} -- \"{path}\"";
}
public List<Models.Change> Result()
{
Exec();

View file

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

View file

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

View file

@ -68,6 +68,18 @@ namespace SourceGit.Commands
return;
}
if (line.StartsWith("deleted file mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(18);
return;
}
if (line.StartsWith("new file mode ", StringComparison.Ordinal))
{
_result.NewMode = line.Substring(14);
return;
}
if (_result.IsBinary)
return;

View file

@ -8,7 +8,26 @@ namespace SourceGit.Commands
{
public static class ExecuteCustomAction
{
public static void Run(string repo, string file, string args, Action<string> outputHandler)
public static void Run(string repo, string file, string args)
{
var start = new ProcessStartInfo();
start.FileName = file;
start.Arguments = args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.WorkingDirectory = repo;
try
{
Process.Start(start);
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
}
public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
{
var start = new ProcessStartInfo();
start.FileName = file;
@ -21,14 +40,6 @@ namespace SourceGit.Commands
start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo;
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
var proc = new Process() { StartInfo = start };
var builder = new StringBuilder();
@ -53,26 +64,21 @@ namespace SourceGit.Commands
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
proc.WaitForExit();
var exitCode = proc.ExitCode;
if (exitCode != 0)
{
var errMsg = builder.ToString().Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, errMsg));
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, e.Message);
});
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
var exitCode = proc.ExitCode;
proc.Close();
if (exitCode != 0)
{
var errMsg = builder.ToString();
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, errMsg);
});
}
}
}
}

View file

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

View file

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Text;
using System.Threading;
using Avalonia.Threading;
namespace SourceGit.Commands
{
/// <summary>
@ -20,82 +22,78 @@ namespace SourceGit.Commands
}
}
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
{
_service = service;
_repo = repo;
_changes = changes;
_cancelToken = cancelToken;
_onProgress = onProgress;
_onResponse = onResponse;
}
public string Result()
public void Exec()
{
try
{
var summarybuilder = new StringBuilder();
var bodyBuilder = new StringBuilder();
_onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
var responseBuilder = new StringBuilder();
var summaryBuilder = new StringBuilder();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
return "";
return;
_onProgress?.Invoke($"Analyzing {change.Path}...");
responseBuilder.Append("- ");
summaryBuilder.Append("- ");
var summary = GenerateChangeSummary(change);
summarybuilder.Append("- ");
summarybuilder.Append(summary);
summarybuilder.Append("(file: ");
summarybuilder.Append(change.Path);
summarybuilder.Append(")");
summarybuilder.AppendLine();
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
if (rs.IsSuccess)
{
_service.Chat(
_service.AnalyzeDiffPrompt,
$"Here is the `git diff` output: {rs.StdOut}",
_cancelToken,
update =>
{
responseBuilder.Append(update);
summaryBuilder.Append(update);
bodyBuilder.Append("- ");
bodyBuilder.Append(summary);
bodyBuilder.AppendLine();
_onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}");
});
}
responseBuilder.Append("\n");
summaryBuilder.Append("(file: ");
summaryBuilder.Append(change.Path);
summaryBuilder.Append(")\n");
}
if (_cancelToken.IsCancellationRequested)
return "";
return;
_onProgress?.Invoke($"Generating commit message...");
var body = bodyBuilder.ToString();
var subject = GenerateSubject(summarybuilder.ToString());
return string.Format("{0}\n\n{1}", subject, body);
var responseBody = responseBuilder.ToString();
var subjectBuilder = new StringBuilder();
_service.Chat(
_service.GenerateSubjectPrompt,
$"Here are the summaries changes:\n{summaryBuilder}",
_cancelToken,
update =>
{
subjectBuilder.Append(update);
_onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
});
}
catch (Exception e)
{
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
return "";
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
}
}
private string GenerateChangeSummary(Models.Change change)
{
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private string GenerateSubject(string summary)
{
var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private Models.OpenAIService _service;
private string _repo;
private List<Models.Change> _changes;
private CancellationToken _cancelToken;
private Action<string> _onProgress;
private Action<string> _onResponse;
}
}

View file

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

View file

@ -0,0 +1,17 @@
namespace SourceGit.Commands
{
public class IsCommitSHA : Command
{
public IsCommitSHA(string repo, string hash)
{
WorkingDirectory = repo;
Args = $"cat-file -t {hash}";
}
public bool Result()
{
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Trim().Equals("commit");
}
}
}

View file

@ -7,7 +7,7 @@ namespace SourceGit.Commands
{
public partial class LFS
{
[GeneratedRegex(@"^(.+)\s+(\w+)\s+\w+:(\d+)$")]
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK();
class SubCmd : Command
@ -82,7 +82,7 @@ namespace SourceGit.Commands
var rs = cmd.ReadToEnd();
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_LOCK().Match(line);

View file

@ -4,21 +4,20 @@ namespace SourceGit.Commands
{
public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, bool prune, Action<string> outputHandler)
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "pull --verbose --progress --tags ";
Args = "pull --verbose --progress ";
if (useRebase)
Args += "--rebase ";
Args += "--rebase=true ";
if (noTags)
Args += "--no-tags ";
if (prune)
Args += "--prune ";
Args += $"{remote} {branch}";
}

View file

@ -26,7 +26,7 @@ namespace SourceGit.Commands
Args += $"{remote} {local}:{remoteBranch}";
}
public Push(string repo, string remote, string tag, bool isDelete)
public Push(string repo, string remote, string refname, bool isDelete)
{
WorkingDirectory = repo;
Context = repo;
@ -36,7 +36,7 @@ namespace SourceGit.Commands
if (isDelete)
Args += "--delete ";
Args += $"{remote} refs/tags/{tag}";
Args += $"{remote} {refname}";
}
protected override void OnReadline(string line)

View file

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

View file

@ -9,10 +9,10 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
Context = repo;
_commit = commit;
Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}";
}
public IEnumerable<string> Result()
public List<string> Result()
{
Exec();
return _lines;

View file

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

View file

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

View file

@ -6,13 +6,11 @@ namespace SourceGit.Commands
{
public class QueryCommits : Command
{
public QueryCommits(string repo, bool useTopoOrder, string limits, bool needFindHead = true)
public QueryCommits(string repo, string limits, bool needFindHead = true)
{
var order = useTopoOrder ? "--topo-order" : "--date-order";
WorkingDirectory = repo;
Context = repo;
Args = $"log {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 {limits}";
Args = $"log --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
_findFirstMerged = needFindHead;
}
@ -20,9 +18,13 @@ namespace SourceGit.Commands
{
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
if (method == Models.CommitSearchMethod.ByUser)
if (method == Models.CommitSearchMethod.ByAuthor)
{
search += $"-i --author=\"{filter}\" --committer=\"{filter}\"";
search += $"-i --author=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByCommitter)
{
search += $"-i --committer=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByFile)
{
@ -33,7 +35,7 @@ namespace SourceGit.Commands
var argsBuilder = new StringBuilder();
argsBuilder.Append(search);
var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
@ -46,7 +48,7 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
Context = repo;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
_findFirstMerged = false;
}
@ -122,7 +124,7 @@ namespace SourceGit.Commands
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0)
return;

View file

@ -3,18 +3,18 @@ using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryCommitsWithFullMessage : Command
public class QueryCommitsForInteractiveRebase : Command
{
public QueryCommitsWithFullMessage(string repo, string args)
public QueryCommitsForInteractiveRebase(string repo, string on)
{
_boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----";
WorkingDirectory = repo;
Context = repo;
Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {args}";
Args = $"log --date-order --no-show-signature --decorate=full --format=\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {on}..HEAD";
}
public List<Models.CommitWithMessage> Result()
public List<Models.InteractiveCommit> Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
@ -29,7 +29,7 @@ namespace SourceGit.Commands
switch (nextPartIdx)
{
case 0:
_current = new Models.CommitWithMessage();
_current = new Models.InteractiveCommit();
_current.Commit.SHA = line;
_commits.Add(_current);
break;
@ -52,7 +52,7 @@ namespace SourceGit.Commands
_current.Commit.CommitterTime = ulong.Parse(line);
break;
default:
var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal);
if (boundary > end)
{
_current.Message = rs.StdOut.Substring(start, boundary - start - 1);
@ -88,8 +88,8 @@ namespace SourceGit.Commands
_current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
private List<Models.CommitWithMessage> _commits = new List<Models.CommitWithMessage>();
private Models.CommitWithMessage _current = null;
private List<Models.InteractiveCommit> _commits = [];
private Models.InteractiveCommit _current = null;
private string _boundary = "";
}
}

View file

@ -0,0 +1,26 @@
using System.IO;
namespace SourceGit.Commands
{
public class QueryGitCommonDir : Command
{
public QueryGitCommonDir(string workDir)
{
WorkingDirectory = workDir;
Args = "rev-parse --git-common-dir";
RaiseError = false;
}
public string Result()
{
var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs))
return null;
rs = rs.Trim();
if (Path.IsPathRooted(rs))
return rs;
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
}
}
}

View file

@ -13,7 +13,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List<Models.Change> Result()

View file

@ -20,9 +20,12 @@ namespace SourceGit.Commands
if (!output.IsSuccess)
return rs;
var lines = output.StdOut.Split('\n');
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.EndsWith("/HEAD", StringComparison.Ordinal))
continue;
if (line.StartsWith("refs/heads/", StringComparison.Ordinal))
rs.Add(new() { Name = line.Substring("refs/heads/".Length), Type = Models.DecoratorType.LocalBranchHead });
else if (line.StartsWith("refs/remotes/", StringComparison.Ordinal))

View file

@ -0,0 +1,21 @@
namespace SourceGit.Commands
{
public class QueryRevisionByRefName : Command
{
public QueryRevisionByRefName(string repo, string refname)
{
WorkingDirectory = repo;
Context = repo;
Args = $"rev-parse {refname}";
}
public string Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut))
return rs.StdOut.Trim();
return null;
}
}
}

View file

@ -1,4 +1,6 @@
namespace SourceGit.Commands
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryRevisionFileNames : Command
{
@ -9,13 +11,17 @@
Args = $"ls-tree -r -z --name-only {revision}";
}
public string[] Result()
public List<string> Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
if (!rs.IsSuccess)
return [];
return [];
var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
var outs = new List<string>();
foreach (var line in lines)
outs.Add(line);
return outs;
}
}
}

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
Args = $"show --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
}
public Models.Commit Result()

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ namespace SourceGit.Commands
Context = repo;
WorkingDirectory = repo;
Args = $"tag -l --sort=-creatordate --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%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()
@ -25,14 +25,15 @@ namespace SourceGit.Commands
foreach (var record in records)
{
var subs = record.Split('\0', StringSplitOptions.None);
if (subs.Length != 4)
if (subs.Length != 5)
continue;
var message = subs[3].Trim();
var message = subs[4].Trim();
tags.Add(new Models.Tag()
{
Name = subs[0].Substring(10),
SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
CreatorDate = ulong.Parse(subs[3]),
Message = string.IsNullOrEmpty(message) ? null : message,
});
}

View file

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

View file

@ -37,6 +37,19 @@ namespace SourceGit.Commands
return true;
}
public static bool ProcessStashChanges(string repo, List<Models.DiffOption> opts, string saveTo)
{
using (var sw = File.Create(saveTo))
{
foreach (var opt in opts)
{
if (!ProcessSingleChange(repo, opt, sw))
return false;
}
}
return true;
}
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
{
var starter = new ProcessStartInfo();

View file

@ -11,72 +11,84 @@ namespace SourceGit.Commands
Context = repo;
}
public bool Push(string message)
{
Args = $"stash push -m \"{message}\"";
return Exec();
}
public bool Push(List<Models.Change> changes, string message, bool onlyStaged, bool keepIndex)
public bool Push(string message, bool includeUntracked = true, bool keepIndex = false)
{
var builder = new StringBuilder();
builder.Append("stash push ");
if (onlyStaged)
builder.Append("--staged ");
if (includeUntracked)
builder.Append("--include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\"");
Args = builder.ToString();
return Exec();
}
public bool Push(string message, List<Models.Change> changes, bool keepIndex)
{
var builder = new StringBuilder();
builder.Append("stash push --include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\" -- ");
if (onlyStaged)
{
foreach (var c in changes)
builder.Append($"\"{c.Path}\" ");
}
else
{
var needAdd = new List<Models.Change>();
foreach (var c in changes)
{
builder.Append($"\"{c.Path}\" ");
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked)
{
needAdd.Add(c);
if (needAdd.Count > 10)
{
new Add(WorkingDirectory, needAdd).Exec();
needAdd.Clear();
}
}
}
if (needAdd.Count > 0)
{
new Add(WorkingDirectory, needAdd).Exec();
needAdd.Clear();
}
}
foreach (var c in changes)
builder.Append($"\"{c.Path}\" ");
Args = builder.ToString();
return Exec();
}
public bool Apply(string name)
public bool Push(string message, string pathspecFromFile, bool keepIndex)
{
Args = $"stash apply -q {name}";
var builder = new StringBuilder();
builder.Append("stash push --include-untracked --pathspec-from-file=\"");
builder.Append(pathspecFromFile);
builder.Append("\" ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\"");
Args = builder.ToString();
return Exec();
}
public bool PushOnlyStaged(string message, bool keepIndex)
{
var builder = new StringBuilder();
builder.Append("stash push --staged ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\"");
Args = builder.ToString();
return Exec();
}
public bool Apply(string name, bool restoreIndex)
{
var opts = restoreIndex ? "--index" : string.Empty;
Args = $"stash apply -q {opts} \"{name}\"";
return Exec();
}
public bool Pop(string name)
{
Args = $"stash pop -q {name}";
Args = $"stash pop -q --index \"{name}\"";
return Exec();
}
public bool Drop(string name)
{
Args = $"stash drop -q {name}";
Args = $"stash drop -q \"{name}\"";
return Exec();
}

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN";
}
public Models.Statistics Result()

View file

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

View file

@ -1,19 +0,0 @@
namespace SourceGit.Commands
{
public class Version : Command
{
public Version()
{
Args = "--version";
RaiseError = false;
}
public string Query()
{
var rs = ReadToEnd();
if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut))
return string.Empty;
return rs.StdOut.Trim().Substring("git version ".Length);
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands
{
@ -20,12 +21,13 @@ namespace SourceGit.Commands
var last = null as Models.Worktree;
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.StartsWith("worktree ", StringComparison.Ordinal))
{
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath);
worktrees.Add(last);
}
else if (line.StartsWith("bare", StringComparison.Ordinal))
@ -73,6 +75,8 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(tracking))
Args += tracking;
else if (!string.IsNullOrEmpty(name) && !createNew)
Args += name;
_outputHandler = outputHandler;
return Exec();

View file

@ -1,4 +1,5 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace SourceGit.Converters
{
@ -6,5 +7,8 @@ namespace SourceGit.Converters
{
public static readonly FuncValueConverter<bool, double> ToPageTabWidth =
new FuncValueConverter<bool, double>(x => x ? 200 : double.NaN);
public static readonly FuncValueConverter<bool, FontWeight> IsBoldToFontWeight =
new FuncValueConverter<bool, FontWeight>(x => x ? FontWeight.Bold : FontWeight.Normal);
}
}

View file

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

View file

@ -1,13 +1,12 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using Avalonia.Data.Converters;
using Avalonia.Styling;
namespace SourceGit.Converters
{
public static partial class StringConverters
public static class StringConverters
{
public class ToLocaleConverter : IValueConverter
{
@ -68,22 +67,6 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<string, string> ToShortSHA =
new FuncValueConverter<string, string>(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v));
public static readonly FuncValueConverter<string, bool> UnderRecommendGitVersion =
new(v =>
{
var match = REG_GIT_VERSION().Match(v ?? "");
if (match.Success)
{
var major = int.Parse(match.Groups[1].Value);
var minor = int.Parse(match.Groups[2].Value);
var build = int.Parse(match.Groups[3].Value);
return new Version(major, minor, build) < MINIMAL_GIT_VERSION;
}
return true;
});
public static readonly FuncValueConverter<string, string> TrimRefsPrefix =
new FuncValueConverter<string, string>(v =>
{
@ -96,9 +79,7 @@ namespace SourceGit.Converters
return v;
});
[GeneratedRegex(@"^[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")]
private static partial Regex REG_GIT_VERSION();
private static readonly Version MINIMAL_GIT_VERSION = new Version(2, 23, 0);
public static readonly FuncValueConverter<string, bool> ContainsSpaces =
new FuncValueConverter<string, bool>(v => v != null && v.Contains(' '));
}
}

View file

@ -2,14 +2,22 @@
{
public class ApplyWhiteSpaceMode
{
public static readonly ApplyWhiteSpaceMode[] Supported =
[
new ApplyWhiteSpaceMode("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
new ApplyWhiteSpaceMode("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
new ApplyWhiteSpaceMode("Error", "Raise errors and refuses to apply the patch", "error"),
new ApplyWhiteSpaceMode("Error All", "Similar to 'error', but shows more", "error-all"),
];
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public ApplyWhiteSpaceMode(string n, string d, string a)
{
Name = App.Text(n);
Desc = App.Text(d);
Name = n;
Desc = d;
Arg = a;
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -35,7 +35,7 @@ namespace SourceGit.Models
private static AvatarManager _instance = null;
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")]
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")]
private static partial Regex REG_GITHUB_USER_EMAIL();
private object _synclock = new object();
@ -43,6 +43,7 @@ namespace SourceGit.Models
private List<IAvatarHost> _avatars = new List<IAvatarHost>();
private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
private HashSet<string> _requesting = new HashSet<string>();
private HashSet<string> _defaultAvatars = new HashSet<string>();
public void Start()
{
@ -50,8 +51,8 @@ namespace SourceGit.Models
if (!Directory.Exists(_storePath))
Directory.CreateDirectory(_storePath);
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute));
_resources.Add("noreply@github.com", new Bitmap(icon));
LoadDefaultAvatar("noreply@github.com", "github.png");
LoadDefaultAvatar("unrealbot@epicgames.com", "unreal.png");
Task.Run(() =>
{
@ -140,7 +141,7 @@ namespace SourceGit.Models
{
if (forceRefetch)
{
if (email.Equals("noreply@github.com", StringComparison.Ordinal))
if (_defaultAvatars.Contains(email))
return null;
if (_resources.ContainsKey(email))
@ -185,11 +186,18 @@ namespace SourceGit.Models
return null;
}
private void LoadDefaultAvatar(string key, string img)
{
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute));
_resources.Add(key, new Bitmap(icon));
_defaultAvatars.Add(key);
}
private string GetEmailHash(string email)
{
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
var builder = new StringBuilder();
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered).AsSpan());
var builder = new StringBuilder(hash.Length * 2);
foreach (var c in hash)
builder.Append(c.ToString("x2"));
return builder.ToString();

View file

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

View file

@ -8,7 +8,9 @@ namespace SourceGit.Models
{
public enum CommitSearchMethod
{
ByUser,
BySHA = 0,
ByAuthor,
ByCommitter,
ByMessage,
ByFile,
}
@ -31,9 +33,10 @@ namespace SourceGit.Models
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
public bool HasDecorators => Decorators.Count > 0;
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd");
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public bool IsMerged { get; set; } = false;
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
@ -111,9 +114,9 @@ namespace SourceGit.Models
}
}
public class CommitWithMessage
public class CommitFullMessage
{
public Commit Commit { get; set; } = new Commit();
public string Message { get; set; } = "";
public string Message { get; set; } = string.Empty;
public List<Hyperlink> Links { get; set; } = [];
}
}

View file

@ -25,10 +25,11 @@ namespace SourceGit.Models
s_penCount = colors.Count;
}
public class Path(int color)
public class Path(int color, bool isMerged)
{
public List<Point> Points { get; } = [];
public int Color { get; } = color;
public bool IsMerged { get; } = isMerged;
}
public class Link
@ -37,6 +38,7 @@ namespace SourceGit.Models
public Point Control;
public Point End;
public int Color;
public bool IsMerged;
}
public enum DotType
@ -51,6 +53,7 @@ namespace SourceGit.Models
public DotType Type;
public Point Center;
public int Color;
public bool IsMerged;
}
public List<Path> Paths { get; } = [];
@ -68,7 +71,7 @@ namespace SourceGit.Models
var unsolved = new List<PathHelper>();
var ended = new List<PathHelper>();
var offsetY = -halfHeight;
var colorIdx = 0;
var colorPicker = new ColorPicker();
foreach (var commit in commits)
{
@ -108,7 +111,6 @@ namespace SourceGit.Models
}
isMerged = isMerged || l.IsMerged;
major.IsMerged = isMerged;
}
else
{
@ -119,28 +121,35 @@ namespace SourceGit.Models
// Remove ended curves from unsolved
foreach (var l in ended)
{
colorPicker.Recycle(l.Path.Color);
unsolved.Remove(l);
}
ended.Clear();
// Create new curve for branch head
// If no path found, create new curve for branch head
// Otherwise, create new curve for new merged commit
if (major == null)
{
offsetX += unitWidth;
if (commit.Parents.Count > 0)
{
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY));
unsolved.Add(major);
temp.Paths.Add(major.Path);
}
colorIdx = (colorIdx + 1) % s_penCount;
}
else if (isMerged && !major.IsMerged && commit.Parents.Count > 0)
{
major.ReplaceMerged();
temp.Paths.Add(major.Path);
}
// Calculate link position of this commit.
var position = new Point(major?.LastX ?? offsetX, offsetY);
var dotColor = major?.Path.Color ?? 0;
var anchor = new Dot() { Center = position, Color = dotColor };
var anchor = new Dot() { Center = position, Color = dotColor, IsMerged = isMerged };
if (commit.IsCurrentHead)
anchor.Type = DotType.Head;
else if (commit.Parents.Count > 1)
@ -158,16 +167,20 @@ namespace SourceGit.Models
var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal));
if (parent != null)
{
// Try to change the merge state of linked graph
if (isMerged)
parent.IsMerged = true;
if (isMerged && !parent.IsMerged)
{
parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight);
parent.ReplaceMerged();
temp.Paths.Add(parent.Path);
}
temp.Links.Add(new Link
{
Start = position,
End = new Point(parent.LastX, offsetY + halfHeight),
Control = new Point(parent.LastX, position.Y),
Color = parent.Path.Color
Color = parent.Path.Color,
IsMerged = isMerged,
});
}
else
@ -175,10 +188,9 @@ namespace SourceGit.Models
offsetX += unitWidth;
// Create new curve for parent commit that not includes before
var l = new PathHelper(parentHash, isMerged, colorIdx, position, new Point(offsetX, position.Y + halfHeight));
var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight));
unsolved.Add(l);
temp.Paths.Add(l.Path);
colorIdx = (colorIdx + 1) % s_penCount;
}
}
}
@ -205,32 +217,53 @@ namespace SourceGit.Models
return temp;
}
private class ColorPicker
{
public int Next()
{
if (_colorsQueue.Count == 0)
{
for (var i = 0; i < s_penCount; i++)
_colorsQueue.Enqueue(i);
}
return _colorsQueue.Dequeue();
}
public void Recycle(int idx)
{
if (!_colorsQueue.Contains(idx))
_colorsQueue.Enqueue(idx);
}
private Queue<int> _colorsQueue = new Queue<int>();
}
private class PathHelper
{
public Path Path { get; }
public Path Path { get; private set; }
public string Next { get; set; }
public bool IsMerged { get; set; }
public double LastX { get; private set; }
public bool IsMerged => Path.IsMerged;
public PathHelper(string next, bool isMerged, int color, Point start)
{
Next = next;
IsMerged = isMerged;
LastX = start.X;
_lastY = start.Y;
Path = new Path(color);
Path = new Path(color, isMerged);
Path.Points.Add(start);
}
public PathHelper(string next, bool isMerged, int color, Point start, Point to)
{
Next = next;
IsMerged = isMerged;
LastX = to.X;
_lastY = to.Y;
Path = new Path(color);
Path = new Path(color, isMerged);
Path.Points.Add(start);
Path.Points.Add(to);
}
@ -310,6 +343,19 @@ namespace SourceGit.Models
_lastY = y;
}
/// <summary>
/// End the current path and create a new from the end.
/// </summary>
public void ReplaceMerged()
{
var color = Path.Color;
Add(LastX, _lastY);
Path = new Path(color, true);
Path.Points.Add(new Point(LastX, _lastY));
_endY = 0;
}
private void Add(double x, double y)
{
if (_endY < y)
@ -327,7 +373,6 @@ namespace SourceGit.Models
private static readonly List<Color> s_defaultPenColors = [
Colors.Orange,
Colors.ForestGreen,
Colors.Gray,
Colors.Turquoise,
Colors.Olive,
Colors.Magenta,

View file

@ -6,6 +6,7 @@ namespace SourceGit.Models
{
Repository,
Commit,
Branch,
}
public class CustomAction : ObservableObject
@ -34,9 +35,16 @@ namespace SourceGit.Models
set => SetProperty(ref _arguments, value);
}
public bool WaitForExit
{
get => _waitForExit;
set => SetProperty(ref _waitForExit, value);
}
private string _name = string.Empty;
private CustomActionScope _scope = CustomActionScope.Repository;
private string _executable = string.Empty;
private string _arguments = string.Empty;
private bool _waitForExit = true;
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
public class DateTimeFormat
{
public string DateOnly { get; set; }
public string DateTime { get; set; }
public string Example
{
get => _example.ToString(DateTime);
}
public DateTimeFormat(string dateOnly, string dateTime)
{
DateOnly = dateOnly;
DateTime = dateTime;
}
public static int ActiveIndex
{
get;
set;
} = 0;
public static DateTimeFormat Actived
{
get => Supported[ActiveIndex];
}
public static readonly List<DateTimeFormat> Supported = new List<DateTimeFormat>
{
new DateTimeFormat("yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss"),
new DateTimeFormat("yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss"),
new DateTimeFormat("yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss"),
new DateTimeFormat("MM/dd/yyyy", "MM/dd/yyyy HH:mm:ss"),
new DateTimeFormat("MM.dd.yyyy", "MM.dd.yyyy HH:mm:ss"),
new DateTimeFormat("MM-dd-yyyy", "MM-dd-yyyy HH:mm:ss"),
new DateTimeFormat("dd/MM/yyyy", "dd/MM/yyyy HH:mm:ss"),
new DateTimeFormat("dd.MM.yyyy", "dd.MM.yyyy HH:mm:ss"),
new DateTimeFormat("dd-MM-yyyy", "dd-MM-yyyy HH:mm:ss"),
new DateTimeFormat("MMM d yyyy", "MMM d yyyy HH:mm:ss"),
new DateTimeFormat("d MMM yyyy", "d MMM yyyy HH:mm:ss"),
};
private static readonly DateTime _example = new DateTime(2025, 1, 31, 8, 0, 0, DateTimeKind.Local);
}
}

View file

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

View file

@ -681,6 +681,18 @@ namespace SourceGit.Models
public TextDiff TextDiff { get; set; } = null;
public LFSDiff LFSDiff { get; set; } = null;
public string FileModeChange => string.IsNullOrEmpty(OldMode) ? string.Empty : $"{OldMode} → {NewMode}";
public string FileModeChange
{
get
{
if (string.IsNullOrEmpty(OldMode) && string.IsNullOrEmpty(NewMode))
return string.Empty;
var oldDisplay = string.IsNullOrEmpty(OldMode) ? "0" : OldMode;
var newDisplay = string.IsNullOrEmpty(NewMode) ? "0" : NewMode;
return $"{oldDisplay} → {newDisplay}";
}
}
}
}

View file

@ -39,7 +39,7 @@ namespace SourceGit.Models
new ExternalMerger(4, "tortoise_merge", "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\""),
new ExternalMerger(5, "kdiff3", "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(6, "beyond_compare", "Beyond Compare", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e -sw \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""),
};

30
src/Models/GitVersions.cs Normal file
View file

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

View file

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

View file

@ -12,6 +12,12 @@ namespace SourceGit.Models
Drop,
}
public class InteractiveCommit
{
public Commit Commit { get; set; } = new Commit();
public string Message { get; set; } = string.Empty;
}
public class InteractiveRebaseJob
{
public string SHA { get; set; } = string.Empty;

View file

@ -5,8 +5,9 @@
public static readonly MergeMode[] Supported =
[
new MergeMode("Default", "Fast-forward if possible", ""),
new MergeMode("Fast-forward", "Refuse to merge when fast-forward is not possible", "--ff-only"),
new MergeMode("No Fast-forward", "Always create a merge commit", "--no-ff"),
new MergeMode("Squash", "Use '--squash'", "--squash"),
new MergeMode("Squash", "Squash merge", "--squash"),
new MergeMode("Don't commit", "Merge without commit", "--no-ff --no-commit"),
];

View file

@ -1,79 +1,99 @@
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading;
using Azure.AI.OpenAI;
using CommunityToolkit.Mvvm.ComponentModel;
using OpenAI;
using OpenAI.Chat;
namespace SourceGit.Models
{
public class OpenAIChatMessage
public partial class OpenAIResponse
{
[JsonPropertyName("role")]
public string Role
public OpenAIResponse(Action<string> onUpdate)
{
get;
set;
_onUpdate = onUpdate;
}
[JsonPropertyName("content")]
public string Content
public void Append(string text)
{
get;
set;
}
}
var buffer = text;
public class OpenAIChatChoice
{
[JsonPropertyName("index")]
public int Index
{
get;
set;
if (_thinkTail.Length > 0)
{
_thinkTail.Append(buffer);
buffer = _thinkTail.ToString();
_thinkTail.Clear();
}
buffer = REG_COT().Replace(buffer, "");
var startIdx = buffer.IndexOf('<', StringComparison.Ordinal);
if (startIdx >= 0)
{
if (startIdx > 0)
OnReceive(buffer.Substring(0, startIdx));
var endIdx = buffer.IndexOf(">", startIdx + 1, StringComparison.Ordinal);
if (endIdx <= startIdx)
{
if (buffer.Length - startIdx <= 15)
_thinkTail.Append(buffer.Substring(startIdx));
else
OnReceive(buffer.Substring(startIdx));
}
else if (endIdx < startIdx + 15)
{
var tag = buffer.Substring(startIdx + 1, endIdx - startIdx - 1);
if (_thinkTags.Contains(tag))
_thinkTail.Append(buffer.Substring(startIdx));
else
OnReceive(buffer.Substring(startIdx));
}
else
{
OnReceive(buffer.Substring(startIdx));
}
}
else
{
OnReceive(buffer);
}
}
[JsonPropertyName("message")]
public OpenAIChatMessage Message
public void End()
{
get;
set;
}
}
public class OpenAIChatResponse
{
[JsonPropertyName("choices")]
public List<OpenAIChatChoice> Choices
{
get;
set;
} = [];
}
public class OpenAIChatRequest
{
[JsonPropertyName("model")]
public string Model
{
get;
set;
if (_thinkTail.Length > 0)
{
OnReceive(_thinkTail.ToString());
_thinkTail.Clear();
}
}
[JsonPropertyName("messages")]
public List<OpenAIChatMessage> Messages
private void OnReceive(string text)
{
get;
set;
} = [];
if (!_hasTrimmedStart)
{
text = text.TrimStart();
if (string.IsNullOrEmpty(text))
return;
public void AddMessage(string role, string content)
{
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
_hasTrimmedStart = true;
}
_onUpdate.Invoke(text);
}
[GeneratedRegex(@"<(think|thought|thinking|thought_chain)>.*?</\1>", RegexOptions.Singleline)]
private static partial Regex REG_COT();
private Action<string> _onUpdate = null;
private StringBuilder _thinkTail = new StringBuilder();
private HashSet<string> _thinkTags = ["think", "thought", "thinking", "thought_chain"];
private bool _hasTrimmedStart = false;
}
public class OpenAIService : ObservableObject
@ -87,7 +107,15 @@ namespace SourceGit.Models
public string Server
{
get => _server;
set => SetProperty(ref _server, value);
set
{
// migrate old server value
if (!string.IsNullOrEmpty(value) && value.EndsWith("/chat/completions", StringComparison.Ordinal))
{
value = value.Substring(0, value.Length - "/chat/completions".Length);
}
SetProperty(ref _server, value);
}
}
public string ApiKey
@ -102,6 +130,12 @@ namespace SourceGit.Models
set => SetProperty(ref _model, value);
}
public bool Streaming
{
get => _streaming;
set => SetProperty(ref _streaming, value);
}
public string AnalyzeDiffPrompt
{
get => _analyzeDiffPrompt;
@ -147,45 +181,54 @@ namespace SourceGit.Models
""";
}
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
public void Chat(string prompt, string question, CancellationToken cancellation, Action<string> onUpdate)
{
var chat = new OpenAIChatRequest() { Model = Model };
chat.AddMessage("user", prompt);
chat.AddMessage("user", question);
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
if (!string.IsNullOrEmpty(ApiKey))
var server = new Uri(_server);
var key = new ApiKeyCredential(_apiKey);
var client = null as ChatClient;
if (_server.Contains("openai.azure.com/", StringComparison.Ordinal))
{
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
client.DefaultRequestHeaders.Add("api-key", ApiKey);
else
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
var azure = new AzureOpenAIClient(server, key);
client = azure.GetChatClient(_model);
}
else
{
var openai = new OpenAIClient(key, new() { Endpoint = server });
client = openai.GetChatClient(_model);
}
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
var messages = new List<ChatMessage>();
messages.Add(_model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt));
messages.Add(new UserChatMessage(question));
try
{
var task = client.PostAsync(Server, req, cancellation);
task.Wait(cancellation);
var rsp = new OpenAIResponse(onUpdate);
var rsp = task.Result;
var reader = rsp.Content.ReadAsStringAsync(cancellation);
reader.Wait(cancellation);
var body = reader.Result;
if (!rsp.IsSuccessStatusCode)
if (_streaming)
{
throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}");
var updates = client.CompleteChatStreaming(messages, null, cancellation);
foreach (var update in updates)
{
if (update.ContentUpdate.Count > 0)
rsp.Append(update.ContentUpdate[0].Text);
}
}
else
{
var completion = client.CompleteChat(messages, null, cancellation);
if (completion.Value.Content.Count > 0)
rsp.Append(completion.Value.Content[0].Text);
}
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
rsp.End();
}
catch
{
if (cancellation.IsCancellationRequested)
return null;
throw;
if (!cancellation.IsCancellationRequested)
throw;
}
}
@ -193,6 +236,7 @@ namespace SourceGit.Models
private string _server;
private string _apiKey;
private string _model;
private bool _streaming = true;
private string _analyzeDiffPrompt;
private string _generateSubjectPrompt;
}

View file

@ -8,12 +8,12 @@ namespace SourceGit.Models
{
[GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")]
private static partial Regex REG_HTTPS();
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.git)?$")]
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
private static partial Regex REG_SSH1();
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~]+/[\w\-\.]+(\.git)?$")]
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
private static partial Regex REG_SSH2();
[GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~]+/[\w\-\.]+)\.git$")]
[GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~%]+/[\w\-\.%]+)\.git$")]
private static partial Regex REG_TO_VISIT_URL_CAPTURE();
private static readonly Regex[] URL_FORMATS = [

View file

@ -14,13 +14,43 @@ namespace SourceGit.Models
set;
} = string.Empty;
public DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch
public bool EnableReflog
{
get;
set;
} = DealWithLocalChanges.DoNothing;
} = false;
public bool EnablePruneOnFetch
public bool EnableFirstParentInHistories
{
get;
set;
} = false;
public bool EnableTopoOrderInHistories
{
get;
set;
} = false;
public bool OnlyHighlighCurrentBranchInHistories
{
get;
set;
} = false;
public TagSortMode TagSortMode
{
get;
set;
} = TagSortMode.CreatorDate;
public bool IncludeUntrackedInLocalChanges
{
get;
set;
} = true;
public bool EnableForceOnFetch
{
get;
set;
@ -32,12 +62,6 @@ namespace SourceGit.Models
set;
} = false;
public DealWithLocalChanges DealWithLocalChangesOnPull
{
get;
set;
} = DealWithLocalChanges.DoNothing;
public bool PreferRebaseInsteadOfMerge
{
get;
@ -68,11 +92,17 @@ namespace SourceGit.Models
set;
} = false;
public DealWithLocalChanges DealWithLocalChangesOnCreateBranch
public bool PushToRemoteWhenCreateTag
{
get;
set;
} = DealWithLocalChanges.DoNothing;
} = true;
public bool PushToRemoteWhenDeleteTag
{
get;
set;
} = false;
public bool CheckoutBranchOnCreateBranch
{
@ -84,31 +114,31 @@ namespace SourceGit.Models
{
get;
set;
} = new AvaloniaList<Filter>();
} = [];
public AvaloniaList<CommitTemplate> CommitTemplates
{
get;
set;
} = new AvaloniaList<CommitTemplate>();
} = [];
public AvaloniaList<string> CommitMessages
{
get;
set;
} = new AvaloniaList<string>();
} = [];
public AvaloniaList<IssueTrackerRule> IssueTrackerRules
{
get;
set;
} = new AvaloniaList<IssueTrackerRule>();
} = [];
public AvaloniaList<CustomAction> CustomActions
{
get;
set;
} = new AvaloniaList<CustomAction>();
} = [];
public bool EnableAutoFetch
{
@ -146,12 +176,54 @@ namespace SourceGit.Models
set;
} = false;
public bool AutoRestoreAfterStash
{
get;
set;
} = false;
public string PreferedOpenAIService
{
get;
set;
} = "---";
public bool IsLocalBranchesExpandedInSideBar
{
get;
set;
} = true;
public bool IsRemotesExpandedInSideBar
{
get;
set;
} = false;
public bool IsTagsExpandedInSideBar
{
get;
set;
} = false;
public bool IsSubmodulesExpandedInSideBar
{
get;
set;
} = false;
public bool IsWorktreeExpandedInSideBar
{
get;
set;
} = false;
public List<string> ExpandedBranchNodesInSideBar
{
get;
set;
} = [];
public Dictionary<string, FilterMode> CollectHistoriesFilters()
{
var map = new Dictionary<string, FilterMode>();
@ -378,65 +450,13 @@ namespace SourceGit.Models
CommitMessages.Insert(0, message);
}
public IssueTrackerRule AddNewIssueTracker()
public IssueTrackerRule AddIssueTracker(string name, string regex, string url)
{
var rule = new IssueTrackerRule()
{
Name = "New Issue Tracker",
RegexString = "#(\\d+)",
URLTemplate = "https://xxx/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddGithubIssueTracker(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "Github ISSUE",
RegexString = "#(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://github.com/username/repository/issues/$1" : $"{repoURL}/issues/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddJiraIssueTracker()
{
var rule = new IssueTrackerRule()
{
Name = "Jira Tracker",
RegexString = "PROJ-(\\d+)",
URLTemplate = "https://jira.yourcompany.com/browse/PROJ-$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddGitLabIssueTracker(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "GitLab ISSUE",
RegexString = "#(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://gitlab.com/username/repository/-/issues/$1" : $"{repoURL}/-/issues/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddGitLabMergeRequestTracker(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "GitLab MR",
RegexString = "!(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://gitlab.com/username/repository/-/merge_requests/$1" : $"{repoURL}/-/merge_requests/$1",
Name = name,
RegexString = regex,
URLTemplate = url,
};
IssueTrackerRules.Add(rule);
@ -451,11 +471,7 @@ namespace SourceGit.Models
public CustomAction AddNewCustomAction()
{
var act = new CustomAction()
{
Name = "Unnamed Custom Action",
};
var act = new CustomAction() { Name = "Unnamed Action" };
CustomActions.Add(act);
return act;
}

View file

@ -29,6 +29,6 @@ namespace SourceGit.Models
public class RevisionSubmodule
{
public Commit Commit { get; set; } = null;
public string FullMessage { get; set; } = string.Empty;
public CommitFullMessage FullMessage { get; set; } = null;
}
}

View file

@ -1,4 +1,5 @@
using System.Reflection;
using System;
using System.Reflection;
using System.Text.Json.Serialization;
namespace SourceGit.Models
@ -32,5 +33,24 @@ namespace SourceGit.Models
}
}
public class AlreadyUpToDate { }
public class AlreadyUpToDate
{
}
public class SelfUpdateFailed
{
public string Reason
{
get;
private set;
}
public SelfUpdateFailed(Exception e)
{
if (e.InnerException is { } inner)
Reason = inner.Message;
else
Reason = e.Message;
}
}
}

View file

@ -41,6 +41,8 @@ namespace SourceGit.Models
{
new ShellOrTerminal("mac-terminal", "Terminal", ""),
new ShellOrTerminal("iterm2", "iTerm", ""),
new ShellOrTerminal("warp", "Warp", ""),
new ShellOrTerminal("ghostty", "Ghostty", "")
};
}
else
@ -55,6 +57,7 @@ namespace SourceGit.Models
new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"),
new ShellOrTerminal("foot", "Foot", "foot"),
new ShellOrTerminal("wezterm", "WezTerm", "wezterm"),
new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"),
new ShellOrTerminal("custom", "Custom", ""),
};
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
@ -6,9 +7,10 @@ namespace SourceGit.Models
{
public string Name { get; set; } = "";
public string SHA { get; set; } = "";
public List<string> Parents { get; set; } = [];
public ulong Time { get; set; } = 0;
public string Message { get; set; } = "";
public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
}
}

View file

@ -2,10 +2,18 @@
namespace SourceGit.Models
{
public enum TagSortMode
{
CreatorDate = 0,
NameInAscending,
NameInDescending,
}
public class Tag : ObservableObject
{
public string Name { get; set; } = string.Empty;
public string SHA { get; set; } = string.Empty;
public ulong CreatorDate { get; set; } = 0;
public string Message { get; set; } = string.Empty;
public FilterMode FilterMode

View file

@ -313,7 +313,7 @@ namespace SourceGit.Models
private static bool IsNameChar(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
// (?) notice or log if variable is not found

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,14 +18,33 @@ namespace SourceGit.Native
DisableDefaultApplicationMenuItems = true,
});
// Fix `PATH` env on macOS.
var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(path))
path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal))
path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path;
var customPathFile = Path.Combine(OS.DataDir, "PATH");
if (File.Exists(customPathFile))
OS.CustomPathEnv = File.ReadAllText(customPathFile).Trim();
{
var env = File.ReadAllText(customPathFile).Trim();
if (!string.IsNullOrEmpty(env))
path = env;
}
Environment.SetEnvironmentVariable("PATH", path);
}
public string FindGitExecutable()
{
return File.Exists("/usr/bin/git") ? "/usr/bin/git" : string.Empty;
var gitPathVariants = new List<string>() {
"/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git", "/opt/homebrew/opt/git/bin/git"
};
foreach (var path in gitPathVariants)
if (File.Exists(path))
return path;
return string.Empty;
}
public string FindTerminal(Models.ShellOrTerminal shell)
@ -36,6 +55,10 @@ namespace SourceGit.Native
return "Terminal";
case "iterm2":
return "iTerm";
case "warp":
return "Warp";
case "ghostty":
return "Ghostty";
}
return string.Empty;

View file

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia;
namespace SourceGit.Native
{
public static class OS
public static partial class OS
{
public interface IBackend
{
@ -22,11 +25,48 @@ namespace SourceGit.Native
void OpenWithDefaultEditor(string file);
}
public static string DataDir { get; private set; } = string.Empty;
public static string GitExecutable { get; set; } = string.Empty;
public static string ShellOrTerminal { get; set; } = string.Empty;
public static List<Models.ExternalTool> ExternalTools { get; set; } = [];
public static string CustomPathEnv { get; set; } = string.Empty;
public static string DataDir
{
get;
private set;
} = string.Empty;
public static string GitExecutable
{
get => _gitExecutable;
set
{
if (_gitExecutable != value)
{
_gitExecutable = value;
UpdateGitVersion();
}
}
}
public static string GitVersionString
{
get;
private set;
} = string.Empty;
public static Version GitVersion
{
get;
private set;
} = new Version(0, 0, 0);
public static string ShellOrTerminal
{
get;
set;
} = string.Empty;
public static List<Models.ExternalTool> ExternalTools
{
get;
set;
} = [];
static OS()
{
@ -55,6 +95,17 @@ namespace SourceGit.Native
public static void SetupDataDir()
{
if (OperatingSystem.IsWindows())
{
var execFile = Process.GetCurrentProcess().MainModule!.FileName;
var portableDir = Path.Combine(Path.GetDirectoryName(execFile), "data");
if (Directory.Exists(portableDir))
{
DataDir = portableDir;
return;
}
}
var osAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
if (string.IsNullOrEmpty(osAppDataDir))
DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit");
@ -111,6 +162,68 @@ namespace SourceGit.Native
_backend.OpenWithDefaultEditor(file);
}
public static string GetAbsPath(string root, string sub)
{
var fullpath = Path.Combine(root, sub);
if (OperatingSystem.IsWindows())
return fullpath.Replace('/', '\\');
return fullpath;
}
private static void UpdateGitVersion()
{
if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable))
{
GitVersionString = string.Empty;
GitVersion = new Version(0, 0, 0);
return;
}
var start = new ProcessStartInfo();
start.FileName = _gitExecutable;
start.Arguments = "--version";
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
var proc = new Process() { StartInfo = start };
try
{
proc.Start();
var rs = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();
if (proc.ExitCode == 0 && !string.IsNullOrWhiteSpace(rs))
{
GitVersionString = rs.Trim();
var match = REG_GIT_VERSION().Match(GitVersionString);
if (match.Success)
{
var major = int.Parse(match.Groups[1].Value);
var minor = int.Parse(match.Groups[2].Value);
var build = int.Parse(match.Groups[3].Value);
GitVersion = new Version(major, minor, build);
GitVersionString = GitVersionString.Substring(11).Trim();
}
}
}
catch
{
// Ignore errors
}
proc.Close();
}
[GeneratedRegex(@"^git version[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")]
private static partial Regex REG_GIT_VERSION();
private static IBackend _backend = null;
private static string _gitExecutable = string.Empty;
}
}

View file

@ -8,6 +8,7 @@ using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
namespace SourceGit.Native
{
@ -26,9 +27,21 @@ namespace SourceGit.Native
internal string szCSDVersion;
}
[DllImport("ntdll")]
[StructLayout(LayoutKind.Sequential)]
internal struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
[DllImport("ntdll.dll")]
private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation);
[DllImport("dwmapi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
@ -140,7 +153,7 @@ namespace SourceGit.Native
public void OpenBrowser(string url)
{
var info = new ProcessStartInfo("cmd", $"/c start {url}");
var info = new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\"");
info.CreateNoWindow = true;
Process.Start(info);
}
@ -202,10 +215,17 @@ namespace SourceGit.Native
private void FixWindowFrameOnWin10(Window w)
{
if (w.WindowState == WindowState.Maximized || w.WindowState == WindowState.FullScreen)
w.SystemDecorations = SystemDecorations.Full;
else if (w.WindowState == WindowState.Normal)
w.SystemDecorations = SystemDecorations.BorderOnly;
// Schedule the DWM frame extension to run in the next render frame
// to ensure proper timing with the window initialization sequence
Dispatcher.UIThread.InvokeAsync(() =>
{
var platformHandle = w.TryGetPlatformHandle();
if (platformHandle == null)
return;
var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 };
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
}, DispatcherPriority.Render);
}
#region EXTERNAL_EDITOR_FINDER

View file

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

View file

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

View file

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

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