mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-06-22 10:55:00 +00:00
Merge commit '55be1ad1ca
' into add_locale_ja_JP
This commit is contained in:
commit
50804fdbc7
288 changed files with 10124 additions and 6428 deletions
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
|
@ -19,14 +19,25 @@ jobs:
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
runtime: osx-arm64
|
runtime: osx-arm64
|
||||||
- name : Linux
|
- name : Linux
|
||||||
os: ubuntu-20.04
|
os: ubuntu-latest
|
||||||
runtime: linux-x64
|
runtime: linux-x64
|
||||||
|
container: ubuntu:20.04
|
||||||
- name : Linux (arm64)
|
- name : Linux (arm64)
|
||||||
os: ubuntu-20.04
|
os: ubuntu-latest
|
||||||
runtime: linux-arm64
|
runtime: linux-arm64
|
||||||
|
container: ubuntu:20.04
|
||||||
name: Build ${{ matrix.name }}
|
name: Build ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
container: ${{ matrix.container || '' }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install common CLI tools
|
||||||
|
if: ${{ startsWith(matrix.runtime, 'linux-') }}
|
||||||
|
run: |
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y sudo
|
||||||
|
sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
|
@ -47,7 +58,7 @@ jobs:
|
||||||
if: ${{ matrix.runtime == 'linux-arm64' }}
|
if: ${{ matrix.runtime == 'linux-arm64' }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
|
sudo apt-get install -y llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build -c Release
|
run: dotnet build -c Release
|
||||||
- name: Publish
|
- name: Publish
|
||||||
|
|
22
.github/workflows/package.yml
vendored
22
.github/workflows/package.yml
vendored
|
@ -7,12 +7,12 @@ on:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
jobs:
|
jobs:
|
||||||
windows-portable:
|
windows:
|
||||||
name: Package portable Windows app
|
name: Package Windows
|
||||||
runs-on: ubuntu-latest
|
runs-on: windows-2019
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runtime: [win-x64, win-arm64]
|
runtime: [ win-x64, win-arm64 ]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -22,10 +22,11 @@ jobs:
|
||||||
name: sourcegit.${{ matrix.runtime }}
|
name: sourcegit.${{ matrix.runtime }}
|
||||||
path: build/SourceGit
|
path: build/SourceGit
|
||||||
- name: Package
|
- name: Package
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ inputs.version }}
|
VERSION: ${{ inputs.version }}
|
||||||
RUNTIME: ${{ matrix.runtime }}
|
RUNTIME: ${{ matrix.runtime }}
|
||||||
run: ./build/scripts/package.windows-portable.sh
|
run: ./build/scripts/package.windows.sh
|
||||||
- name: Upload package artifact
|
- name: Upload package artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
@ -36,7 +37,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: sourcegit.${{ matrix.runtime }}
|
name: sourcegit.${{ matrix.runtime }}
|
||||||
osx-app:
|
osx-app:
|
||||||
name: Package OSX app
|
name: Package macOS
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -69,6 +70,7 @@ jobs:
|
||||||
linux:
|
linux:
|
||||||
name: Package Linux
|
name: Package Linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: ubuntu:20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runtime: [linux-x64, linux-arm64]
|
runtime: [linux-x64, linux-arm64]
|
||||||
|
@ -77,9 +79,10 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Download package dependencies
|
- name: Download package dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo add-apt-repository universe
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
sudo apt-get update
|
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||||
sudo apt-get install desktop-file-utils rpm libfuse2
|
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
|
- name: Download build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
@ -89,6 +92,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ inputs.version }}
|
VERSION: ${{ inputs.version }}
|
||||||
RUNTIME: ${{ matrix.runtime }}
|
RUNTIME: ${{ matrix.runtime }}
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||||
run: |
|
run: |
|
||||||
mkdir build/SourceGit
|
mkdir build/SourceGit
|
||||||
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
|
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
|
||||||
|
|
39
.github/workflows/publish-packages.yml
vendored
39
.github/workflows/publish-packages.yml
vendored
|
@ -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
|
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
@ -24,12 +24,6 @@ jobs:
|
||||||
uses: ./.github/workflows/package.yml
|
uses: ./.github/workflows/package.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.version.outputs.version }}
|
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:
|
release:
|
||||||
needs: [package, version]
|
needs: [package, version]
|
||||||
name: Release
|
name: Release
|
||||||
|
@ -44,7 +38,7 @@ jobs:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
VERSION: ${{ needs.version.outputs.version }}
|
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
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
4
LICENSE
4
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
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
|
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
|
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
|
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
|
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
|
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.
|
||||||
|
|
87
README.md
87
README.md
|
@ -18,9 +18,9 @@
|
||||||
* Supports SSH access with each remote
|
* Supports SSH access with each remote
|
||||||
* GIT commands with GUI
|
* GIT commands with GUI
|
||||||
* Clone/Fetch/Pull/Push...
|
* Clone/Fetch/Pull/Push...
|
||||||
* Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
|
* Merge/Rebase/Reset/Revert/Cherry-pick...
|
||||||
* Amend/Reword
|
* Amend/Reword/Squash
|
||||||
* Interactive rebase (Basic)
|
* Interactive rebase
|
||||||
* Branches
|
* Branches
|
||||||
* Remotes
|
* Remotes
|
||||||
* Tags
|
* Tags
|
||||||
|
@ -40,6 +40,7 @@
|
||||||
* Git LFS
|
* Git LFS
|
||||||
* Issue Link
|
* Issue Link
|
||||||
* Workspace
|
* Workspace
|
||||||
|
* Custom Action
|
||||||
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
|
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
@ -47,7 +48,7 @@
|
||||||
|
|
||||||
## Translation Status
|
## Translation Status
|
||||||
|
|
||||||
[](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md)
|
You can find the current translation status in [TRANSLATION.md](TRANSLATION.md)
|
||||||
|
|
||||||
## How to Use
|
## How to Use
|
||||||
|
|
||||||
|
@ -59,12 +60,13 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD
|
||||||
|
|
||||||
| OS | PATH |
|
| OS | PATH |
|
||||||
|---------|-----------------------------------------------------|
|
|---------|-----------------------------------------------------|
|
||||||
| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` |
|
| Windows | `%APPDATA%\SourceGit` |
|
||||||
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
|
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
|
||||||
| macOS | `${HOME}/Library/Application Support/SourceGit` |
|
| macOS | `${HOME}/Library/Application Support/SourceGit` |
|
||||||
|
|
||||||
> [!TIP]
|
> [!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:
|
For **Windows** users:
|
||||||
|
|
||||||
|
@ -75,12 +77,12 @@ For **Windows** users:
|
||||||
```
|
```
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
|
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
|
||||||
* You can install the latest stable by `scoope` with follow commands:
|
* You can install the latest stable by `scoop` with follow commands:
|
||||||
```shell
|
```shell
|
||||||
scoop bucket add extras
|
scoop bucket add extras
|
||||||
scoop install sourcegit
|
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:
|
For **macOS** users:
|
||||||
|
|
||||||
|
@ -98,49 +100,45 @@ For **macOS** users:
|
||||||
|
|
||||||
For **Linux** users:
|
For **Linux** users:
|
||||||
|
|
||||||
* For Debian/Ubuntu based distributions, you can add the `sourcegit` repository by following:
|
* Thanks [@aikawayataro](https://github.com/aikawayataro) for providing `rpm` and `deb` repositories, hosted on [Codeberg](https://codeberg.org/yataro/-/packages).
|
||||||
You may need to install curl and/or gpg first, if you're on a very minimal host:
|
|
||||||
|
`deb` how to:
|
||||||
```shell
|
```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
|
```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
|
If your distribution isn't using `dnf`, please refer to the documentation of your distribution on how to add an `rpm` repository.
|
||||||
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
|
* `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.
|
||||||
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.
|
|
||||||
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
|
* 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
|
## 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`:
|
For `OpenAI`:
|
||||||
|
|
||||||
* `Server` must be `https://api.openai.com/v1/chat/completions`
|
* `Server` must be `https://api.openai.com/v1`
|
||||||
|
|
||||||
For other AI service:
|
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
|
* The `API Key` is optional that depends on the service
|
||||||
|
|
||||||
## External Tools
|
## External Tools
|
||||||
|
@ -159,7 +157,7 @@ This app supports open repository in external tools listed in the table below.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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.
|
> 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
|
```json
|
||||||
{
|
{
|
||||||
"tools": {
|
"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`.
|
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.
|
Thanks to all the people who contribute.
|
||||||
|
|
||||||
[](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
|
[](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
|
||||||
|
|
||||||
|
## Third-Party Components
|
||||||
|
|
||||||
|
For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md).
|
||||||
|
|
|
@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
|
||||||
.github\workflows\package.yml = .github\workflows\package.yml
|
.github\workflows\package.yml = .github\workflows\package.yml
|
||||||
.github\workflows\release.yml = .github\workflows\release.yml
|
.github\workflows\release.yml = .github\workflows\release.yml
|
||||||
.github\workflows\localization-check.yml = .github\workflows\localization-check.yml
|
.github\workflows\localization-check.yml = .github\workflows\localization-check.yml
|
||||||
.github\workflows\publish-packages.yml = .github\workflows\publish-packages.yml
|
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}"
|
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}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control
|
build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control
|
||||||
|
build\resources\deb\DEBIAN\preinst = build\resources\deb\DEBIAN\preinst
|
||||||
|
build\resources\deb\DEBIAN\prerm = build\resources\deb\DEBIAN\prerm
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}"
|
||||||
|
@ -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\localization-check.js = build\scripts\localization-check.js
|
||||||
build\scripts\package.linux.sh = build\scripts\package.linux.sh
|
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.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
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
|
86
THIRD-PARTY-LICENSES.md
Normal file
86
THIRD-PARTY-LICENSES.md
Normal 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
|
249
TRANSLATION.md
249
TRANSLATION.md
|
@ -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
|
||||||
|
|
||||||
|
### 
|
||||||
|
|
||||||
|
### 
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Missing Keys</summary>
|
<summary>Missing keys in de_DE.axaml</summary>
|
||||||
|
|
||||||
- Text.BranchCM.MergeMultiBranches
|
- Text.BranchUpstreamInvalid
|
||||||
- Text.CommitCM.Merge
|
- Text.Configure.CustomAction.WaitForExit
|
||||||
- Text.CommitCM.MergeMultiple
|
- Text.Configure.IssueTracker.AddSampleAzure
|
||||||
- Text.CommitDetail.Files.Search
|
- Text.CopyFullPath
|
||||||
- Text.Diff.UseBlockNavigation
|
- Text.Diff.First
|
||||||
- Text.FileCM.ResolveUsing
|
- Text.Diff.Last
|
||||||
- Text.Hotkeys.Global.Clone
|
- Text.Preferences.AI.Streaming
|
||||||
- Text.InProgress.CherryPick.Head
|
- Text.Preferences.Appearance.EditorTabWidth
|
||||||
- Text.InProgress.Merge.Operating
|
- Text.Preferences.General.ShowTagsInGraph
|
||||||
- Text.InProgress.Rebase.StoppedAt
|
- Text.StashCM.SaveAsPatch
|
||||||
- Text.InProgress.Revert.Head
|
|
||||||
- Text.Merge.Source
|
|
||||||
- Text.MergeMultiple
|
|
||||||
- Text.MergeMultiple.CommitChanges
|
|
||||||
- Text.MergeMultiple.Strategy
|
|
||||||
- Text.MergeMultiple.Targets
|
|
||||||
- Text.Repository.Skip
|
|
||||||
- Text.WorkingCopy.CommitToEdit
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### es_ES.axaml: 97.78%
|
### 
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Missing Keys</summary>
|
<summary>Missing keys in es_ES.axaml</summary>
|
||||||
|
|
||||||
- Text.BranchCM.MergeMultiBranches
|
- Text.CopyFullPath
|
||||||
- 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
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### fr_FR.axaml: 95.00%
|
### 
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Missing Keys</summary>
|
<summary>Missing keys in fr_FR.axaml</summary>
|
||||||
|
|
||||||
- Text.BranchCM.MergeMultiBranches
|
- Text.CopyFullPath
|
||||||
- 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
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### it_IT.axaml: 95.56%
|
### 
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Missing Keys</summary>
|
<summary>Missing keys in it_IT.axaml</summary>
|
||||||
|
|
||||||
|
- Text.CopyFullPath
|
||||||
|
- Text.Preferences.General.ShowTagsInGraph
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 
|
||||||
|
|
||||||
|
<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.BranchCM.MergeMultiBranches
|
||||||
|
- Text.BranchUpstreamInvalid
|
||||||
|
- Text.Clone.RecurseSubmodules
|
||||||
- Text.CommitCM.Merge
|
- Text.CommitCM.Merge
|
||||||
- Text.CommitCM.MergeMultiple
|
- Text.CommitCM.MergeMultiple
|
||||||
- Text.CommitDetail.Files.Search
|
- Text.CommitDetail.Files.Search
|
||||||
- Text.CommitDetail.Info.Children
|
- Text.CommitDetail.Info.Children
|
||||||
- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
|
- Text.Configure.CustomAction.Scope.Branch
|
||||||
- Text.Configure.OpenAI.Preferred
|
- Text.Configure.CustomAction.WaitForExit
|
||||||
- Text.Configure.OpenAI.Preferred.Tip
|
- 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.Diff.UseBlockNavigation
|
||||||
- Text.Fetch.Force
|
- Text.Fetch.Force
|
||||||
- Text.FileCM.ResolveUsing
|
- Text.FileCM.ResolveUsing
|
||||||
|
- Text.Hotkeys.Global.Clone
|
||||||
- Text.InProgress.CherryPick.Head
|
- Text.InProgress.CherryPick.Head
|
||||||
- Text.InProgress.Merge.Operating
|
- Text.InProgress.Merge.Operating
|
||||||
- Text.InProgress.Rebase.StoppedAt
|
- Text.InProgress.Rebase.StoppedAt
|
||||||
|
@ -121,93 +95,40 @@
|
||||||
- Text.MergeMultiple.CommitChanges
|
- Text.MergeMultiple.CommitChanges
|
||||||
- Text.MergeMultiple.Strategy
|
- Text.MergeMultiple.Strategy
|
||||||
- Text.MergeMultiple.Targets
|
- 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
|
||||||
- Text.Repository.FilterCommits.Default
|
- Text.Repository.HistoriesLayout
|
||||||
- Text.Repository.FilterCommits.Exclude
|
- Text.Repository.HistoriesLayout.Horizontal
|
||||||
- Text.Repository.FilterCommits.Include
|
- Text.Repository.HistoriesLayout.Vertical
|
||||||
- Text.Repository.HistoriesOrder
|
- Text.Repository.HistoriesOrder
|
||||||
- Text.Repository.HistoriesOrder.ByDate
|
- Text.Repository.Notifications.Clear
|
||||||
- Text.Repository.HistoriesOrder.Topo
|
- Text.Repository.OnlyHighlightCurrentBranchInHistories
|
||||||
- Text.Repository.Skip
|
- 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.SHALinkCM.NavigateTo
|
||||||
|
- Text.Stash.AutoRestore
|
||||||
|
- Text.Stash.AutoRestore.Tip
|
||||||
|
- Text.StashCM.SaveAsPatch
|
||||||
- Text.WorkingCopy.CommitToEdit
|
- Text.WorkingCopy.CommitToEdit
|
||||||
|
- Text.WorkingCopy.SignOff
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### pt_BR.axaml: 96.81%
|
### 
|
||||||
|
|
||||||
|
### 
|
||||||
|
|
||||||
<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>
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
8.42
|
2025.11
|
|
@ -1,7 +1,8 @@
|
||||||
Package: sourcegit
|
Package: sourcegit
|
||||||
Version: 8.23
|
Version: 2025.10
|
||||||
Priority: optional
|
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
|
Architecture: amd64
|
||||||
|
Installed-Size: 60440
|
||||||
Maintainer: longshuang@msn.cn
|
Maintainer: longshuang@msn.cn
|
||||||
Description: Open-source & Free Git GUI Client
|
Description: Open-source & Free Git GUI Client
|
||||||
|
|
32
build/resources/deb/DEBIAN/preinst
Executable file
32
build/resources/deb/DEBIAN/preinst
Executable 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
|
35
build/resources/deb/DEBIAN/prerm
Executable file
35
build/resources/deb/DEBIAN/prerm
Executable 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
|
|
@ -8,6 +8,7 @@ Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.
|
||||||
Requires: libX11.so.6()(%{__isa_bits}bit)
|
Requires: libX11.so.6()(%{__isa_bits}bit)
|
||||||
Requires: libSM.so.6()(%{__isa_bits}bit)
|
Requires: libSM.so.6()(%{__isa_bits}bit)
|
||||||
Requires: libicu
|
Requires: libicu
|
||||||
|
Requires: xdg-utils
|
||||||
|
|
||||||
%define _build_id_links none
|
%define _build_id_links none
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ const repoRoot = path.join(__dirname, '../../');
|
||||||
const localesDir = path.join(repoRoot, 'src/Resources/Locales');
|
const localesDir = path.join(repoRoot, 'src/Resources/Locales');
|
||||||
const enUSFile = path.join(localesDir, 'en_US.axaml');
|
const enUSFile = path.join(localesDir, 'en_US.axaml');
|
||||||
const outputFile = path.join(repoRoot, 'TRANSLATION.md');
|
const outputFile = path.join(repoRoot, 'TRANSLATION.md');
|
||||||
const readmeFile = path.join(repoRoot, 'README.md');
|
|
||||||
|
|
||||||
const parser = new xml2js.Parser();
|
const parser = new xml2js.Parser();
|
||||||
|
|
||||||
|
@ -18,42 +17,36 @@ async function parseXml(filePath) {
|
||||||
async function calculateTranslationRate() {
|
async function calculateTranslationRate() {
|
||||||
const enUSData = await parseXml(enUSFile);
|
const enUSData = await parseXml(enUSFile);
|
||||||
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
|
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
|
||||||
|
|
||||||
const translationRates = [];
|
|
||||||
const badges = [];
|
|
||||||
|
|
||||||
const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml'));
|
const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml'));
|
||||||
|
|
||||||
// Add en_US badge first
|
const lines = [];
|
||||||
badges.push(`[](TRANSLATION.md)`);
|
|
||||||
|
lines.push('# Translation Status');
|
||||||
|
lines.push('This document shows the translation status of each locale file in the repository.');
|
||||||
|
lines.push(`## Details`);
|
||||||
|
lines.push(`### `);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
const locale = file.replace('.axaml', '').replace('_', '__');
|
||||||
const filePath = path.join(localesDir, file);
|
const filePath = path.join(localesDir, file);
|
||||||
const localeData = await parseXml(filePath);
|
const localeData = await parseXml(filePath);
|
||||||
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
|
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
|
||||||
|
|
||||||
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
|
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
|
||||||
const translationRate = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
|
|
||||||
|
|
||||||
translationRates.push(`### ${file}: ${translationRate.toFixed(2)}%\n`);
|
if (missingKeys.length > 0) {
|
||||||
translationRates.push(`<details>\n<summary>Missing Keys</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`);
|
const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
|
||||||
|
const badgeColor = progress >= 75 ? 'yellow' : 'red';
|
||||||
|
|
||||||
// Add badges
|
lines.push(`### }%25-${badgeColor})`);
|
||||||
const locale = file.replace('.axaml', '').replace('_', '__');
|
lines.push(`<details>\n<summary>Missing keys in ${file}</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`)
|
||||||
const badgeColor = translationRate === 100 ? 'brightgreen' : translationRate >= 75 ? 'yellow' : 'red';
|
} else {
|
||||||
badges.push(`[}%25-${badgeColor})](TRANSLATION.md)`);
|
lines.push(`### `);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(translationRates.join('\n\n'));
|
const content = lines.join('\n\n');
|
||||||
|
console.log(content);
|
||||||
await fs.writeFile(outputFile, translationRates.join('\n\n') + '\n', 'utf8');
|
await fs.writeFile(outputFile, content, 'utf8');
|
||||||
|
|
||||||
// Update README.md
|
|
||||||
let readmeContent = await fs.readFile(readmeFile, 'utf8');
|
|
||||||
const badgeSection = `## Translation Status\n\n${badges.join(' ')}`;
|
|
||||||
console.log(badgeSection);
|
|
||||||
readmeContent = readmeContent.replace(/## Translation Status\n\n.*\n\n/, badgeSection + '\n\n');
|
|
||||||
await fs.writeFile(readmeFile, readmeContent, 'utf8');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateTranslationRate().catch(err => console.error(err));
|
calculateTranslationRate().catch(err => console.error(err));
|
||||||
|
|
|
@ -56,8 +56,15 @@ cp -f SourceGit/* resources/deb/opt/sourcegit
|
||||||
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
|
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
|
||||||
cp -r resources/_common/applications resources/deb/usr/share
|
cp -r resources/_common/applications resources/deb/usr/share
|
||||||
cp -r resources/_common/icons resources/deb/usr/share
|
cp -r resources/_common/icons resources/deb/usr/share
|
||||||
sed -i -e "s/^Version:.*/Version: $VERSION/" -e "s/^Architecture:.*/Architecture: $arch/" resources/deb/DEBIAN/control
|
# Calculate installed size in KB
|
||||||
dpkg-deb --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
|
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"
|
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
|
||||||
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./
|
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./
|
||||||
|
|
|
@ -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
|
|
16
build/scripts/package.windows.sh
Executable file
16
build/scripts/package.windows.sh
Executable 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
|
|
@ -25,12 +25,34 @@ namespace SourceGit
|
||||||
private Action<object> _action = null;
|
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 OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
|
||||||
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
|
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
|
||||||
public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
|
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 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,11 +46,9 @@ namespace SourceGit
|
||||||
[JsonSerializable(typeof(Models.ExternalToolPaths))]
|
[JsonSerializable(typeof(Models.ExternalToolPaths))]
|
||||||
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
|
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
|
||||||
[JsonSerializable(typeof(Models.JetBrainsState))]
|
[JsonSerializable(typeof(Models.JetBrainsState))]
|
||||||
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
|
|
||||||
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
|
|
||||||
[JsonSerializable(typeof(Models.ThemeOverrides))]
|
[JsonSerializable(typeof(Models.ThemeOverrides))]
|
||||||
[JsonSerializable(typeof(Models.Version))]
|
[JsonSerializable(typeof(Models.Version))]
|
||||||
[JsonSerializable(typeof(Models.RepositorySettings))]
|
[JsonSerializable(typeof(Models.RepositorySettings))]
|
||||||
[JsonSerializable(typeof(ViewModels.Preference))]
|
[JsonSerializable(typeof(ViewModels.Preferences))]
|
||||||
internal partial class JsonCodeGen : JsonSerializerContext { }
|
internal partial class JsonCodeGen : JsonSerializerContext { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,9 @@
|
||||||
<NativeMenu>
|
<NativeMenu>
|
||||||
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
|
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
|
||||||
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
|
<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/>
|
<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}"/>
|
<NativeMenuItem Header="{DynamicResource Text.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
|
||||||
<NativeMenuItemSeparator/>
|
<NativeMenuItemSeparator/>
|
||||||
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>
|
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>
|
||||||
|
|
281
src/App.axaml.cs
281
src/App.axaml.cs
|
@ -1,10 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
@ -22,6 +24,7 @@ namespace SourceGit
|
||||||
{
|
{
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
#region App Entry Point
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
@ -34,15 +37,14 @@ namespace SourceGit
|
||||||
|
|
||||||
TaskScheduler.UnobservedTaskException += (_, e) =>
|
TaskScheduler.UnobservedTaskException += (_, e) =>
|
||||||
{
|
{
|
||||||
LogException(e.Exception);
|
|
||||||
e.SetObserved();
|
e.SetObserved();
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo))
|
if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo))
|
||||||
Environment.Exit(exitTodo);
|
Environment.Exit(exitTodo);
|
||||||
else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage))
|
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
|
||||||
Environment.Exit(exitMessage);
|
Environment.Exit(exitMessage);
|
||||||
else
|
else
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
@ -75,34 +77,33 @@ namespace SourceGit
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize()
|
private static void LogException(Exception ex)
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
if (ex == null)
|
||||||
|
return;
|
||||||
|
|
||||||
var pref = ViewModels.Preference.Instance;
|
var builder = new StringBuilder();
|
||||||
pref.PropertyChanged += (_, _) => pref.Save();
|
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);
|
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||||
SetTheme(pref.Theme, pref.ThemeOverrides);
|
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
|
||||||
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
|
File.WriteAllText(file, builder.ToString());
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
|
||||||
{
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
{
|
|
||||||
BindingPlugins.DataValidators.RemoveAt(0);
|
|
||||||
|
|
||||||
if (TryLaunchedAsCoreEditor(desktop))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (TryLaunchedAsAskpass(desktop))
|
|
||||||
return;
|
|
||||||
|
|
||||||
TryLaunchedAsNormal(desktop);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility Functions
|
||||||
public static void OpenDialog(Window window)
|
public static void OpenDialog(Window window)
|
||||||
{
|
{
|
||||||
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||||
|
@ -204,6 +205,9 @@ namespace SourceGit
|
||||||
app._fontsOverrides = null;
|
app._fontsOverrides = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultFont = app.FixFontFamilyName(defaultFont);
|
||||||
|
monospaceFont = app.FixFontFamilyName(monospaceFont);
|
||||||
|
|
||||||
var resDic = new ResourceDictionary();
|
var resDic = new ResourceDictionary();
|
||||||
if (!string.IsNullOrEmpty(defaultFont))
|
if (!string.IsNullOrEmpty(defaultFont))
|
||||||
resDic.Add("Fonts.Default", new FontFamily(defaultFont));
|
resDic.Add("Fonts.Default", new FontFamily(defaultFont));
|
||||||
|
@ -304,21 +308,6 @@ namespace SourceGit
|
||||||
return Current is App app ? app._launcher : null;
|
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)
|
public static void Quit(int exitCode)
|
||||||
{
|
{
|
||||||
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
@ -331,94 +320,39 @@ namespace SourceGit
|
||||||
Environment.Exit(exitCode);
|
Environment.Exit(exitCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
private static void CopyTextBlock(TextBlock textBlock)
|
#region Overrides
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
if (textBlock == null)
|
AvaloniaXamlLoader.Load(this);
|
||||||
return;
|
|
||||||
|
|
||||||
if (textBlock.Inlines is { Count: > 0 } inlines)
|
var pref = ViewModels.Preferences.Instance;
|
||||||
CopyText(inlines.Text);
|
pref.PropertyChanged += (_, _) => pref.Save();
|
||||||
else if (!string.IsNullOrEmpty(textBlock.Text))
|
|
||||||
CopyText(textBlock.Text);
|
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)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
return;
|
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
|
|
||||||
builder.Append("----------------------------\n");
|
|
||||||
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
|
|
||||||
builder.Append($"OS: {Environment.OSVersion.ToString()}\n");
|
|
||||||
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
|
|
||||||
builder.Append($"Source: {ex.Source}\n");
|
|
||||||
builder.Append($"---------------------------\n\n");
|
|
||||||
builder.Append(ex.StackTrace);
|
|
||||||
while (ex.InnerException != null)
|
|
||||||
{
|
{
|
||||||
ex = ex.InnerException;
|
BindingPlugins.DataValidators.RemoveAt(0);
|
||||||
builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n");
|
|
||||||
builder.Append(ex.StackTrace);
|
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)
|
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
exitCode = -1;
|
exitCode = -1;
|
||||||
|
|
||||||
|
@ -471,7 +405,7 @@ namespace SourceGit
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode)
|
private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCode)
|
||||||
{
|
{
|
||||||
exitCode = -1;
|
exitCode = -1;
|
||||||
|
|
||||||
|
@ -505,7 +439,7 @@ namespace SourceGit
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
|
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
var args = desktop.Args;
|
var args = desktop.Args;
|
||||||
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
|
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
|
||||||
|
@ -513,14 +447,18 @@ namespace SourceGit
|
||||||
|
|
||||||
var file = args[1];
|
var file = args[1];
|
||||||
if (!File.Exists(file))
|
if (!File.Exists(file))
|
||||||
|
{
|
||||||
desktop.Shutdown(-1);
|
desktop.Shutdown(-1);
|
||||||
else
|
return true;
|
||||||
desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
|
}
|
||||||
|
|
||||||
|
var editor = new Views.StandaloneCommitMessageEditor();
|
||||||
|
editor.SetFile(file);
|
||||||
|
desktop.MainWindow = editor;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
|
private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS");
|
var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS");
|
||||||
if (launchAsAskpass is not "TRUE")
|
if (launchAsAskpass is not "TRUE")
|
||||||
|
@ -529,14 +467,16 @@ namespace SourceGit
|
||||||
var args = desktop.Args;
|
var args = desktop.Args;
|
||||||
if (args?.Length > 0)
|
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 true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
|
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
Native.OS.SetupEnternalTools();
|
Native.OS.SetupEnternalTools();
|
||||||
Models.AvatarManager.Instance.Start();
|
Models.AvatarManager.Instance.Start();
|
||||||
|
@ -548,9 +488,96 @@ namespace SourceGit
|
||||||
_launcher = new ViewModels.Launcher(startupRepo);
|
_launcher = new ViewModels.Launcher(startupRepo);
|
||||||
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
|
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
|
||||||
|
|
||||||
var pref = ViewModels.Preference.Instance;
|
#if !DISABLE_UPDATE_DETECTION
|
||||||
|
var pref = ViewModels.Preferences.Instance;
|
||||||
if (pref.ShouldCheck4UpdateOnStartup())
|
if (pref.ShouldCheck4UpdateOnStartup())
|
||||||
Check4Update();
|
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;
|
private ViewModels.Launcher _launcher = null;
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace SourceGit.Commands
|
||||||
Args = includeUntracked ? "add ." : "add -u .";
|
Args = includeUntracked ? "add ." : "add -u .";
|
||||||
}
|
}
|
||||||
|
|
||||||
public Add(string repo, List<Models.Change> changes)
|
public Add(string repo, List<string> changes)
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
|
@ -22,10 +22,17 @@ namespace SourceGit.Commands
|
||||||
foreach (var c in changes)
|
foreach (var c in changes)
|
||||||
{
|
{
|
||||||
builder.Append(" \"");
|
builder.Append(" \"");
|
||||||
builder.Append(c.Path);
|
builder.Append(c);
|
||||||
builder.Append("\"");
|
builder.Append("\"");
|
||||||
}
|
}
|
||||||
Args = builder.ToString();
|
Args = builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Add(string repo, string pathspecFromFile)
|
||||||
|
{
|
||||||
|
WorkingDirectory = repo;
|
||||||
|
Context = repo;
|
||||||
|
Args = $"add --pathspec-from-file=\"{pathspecFromFile}\"";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ namespace SourceGit.Commands
|
||||||
var commit = match.Groups[1].Value;
|
var commit = match.Groups[1].Value;
|
||||||
var author = match.Groups[2].Value;
|
var author = match.Groups[2].Value;
|
||||||
var timestamp = int.Parse(match.Groups[3].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()
|
var info = new Models.BlameLineInfo()
|
||||||
{
|
{
|
||||||
|
@ -87,6 +87,7 @@ namespace SourceGit.Commands
|
||||||
|
|
||||||
private readonly Models.BlameData _result = new Models.BlameData();
|
private readonly Models.BlameData _result = new Models.BlameData();
|
||||||
private readonly StringBuilder _content = new StringBuilder();
|
private readonly StringBuilder _content = new StringBuilder();
|
||||||
|
private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
|
||||||
private string _lastSHA = string.Empty;
|
private string _lastSHA = string.Empty;
|
||||||
private bool _needUnifyCommitSHA = false;
|
private bool _needUnifyCommitSHA = false;
|
||||||
private int _minSHALen = 64;
|
private int _minSHALen = 64;
|
||||||
|
|
|
@ -54,21 +54,14 @@
|
||||||
|
|
||||||
public static bool DeleteRemote(string repo, string remote, string name)
|
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();
|
var cmd = new Command();
|
||||||
cmd.WorkingDirectory = repo;
|
cmd.WorkingDirectory = repo;
|
||||||
cmd.Context = repo;
|
cmd.Context = repo;
|
||||||
|
cmd.Args = $"branch -D -r {remote}/{name}";
|
||||||
bool exists = new Remote(repo).HasBranch(remote, name);
|
|
||||||
if (exists)
|
|
||||||
{
|
|
||||||
cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
|
||||||
cmd.Args = $"push {remote} --delete {name}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cmd.Args = $"branch -D -r {remote}/{name}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd.Exec();
|
return cmd.Exec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace SourceGit.Commands
|
||||||
WorkingDirectory = path;
|
WorkingDirectory = path;
|
||||||
TraitErrorAsOutput = true;
|
TraitErrorAsOutput = true;
|
||||||
SSHKey = sshKey;
|
SSHKey = sshKey;
|
||||||
Args = "clone --progress --verbose --recurse-submodules ";
|
Args = "clone --progress --verbose ";
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(extraArgs))
|
if (!string.IsNullOrEmpty(extraArgs))
|
||||||
Args += $"{extraArgs} ";
|
Args += $"{extraArgs} ";
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
@ -10,11 +11,6 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public partial class Command
|
public partial class Command
|
||||||
{
|
{
|
||||||
public class CancelToken
|
|
||||||
{
|
|
||||||
public bool Requested { get; set; } = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ReadToEndResult
|
public class ReadToEndResult
|
||||||
{
|
{
|
||||||
public bool IsSuccess { get; set; } = false;
|
public bool IsSuccess { get; set; } = false;
|
||||||
|
@ -30,7 +26,7 @@ namespace SourceGit.Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Context { get; set; } = string.Empty;
|
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 string WorkingDirectory { get; set; } = null;
|
||||||
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
|
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
|
||||||
public string SSHKey { get; set; } = string.Empty;
|
public string SSHKey { get; set; } = string.Empty;
|
||||||
|
@ -43,36 +39,15 @@ namespace SourceGit.Commands
|
||||||
var start = CreateGitStartInfo();
|
var start = CreateGitStartInfo();
|
||||||
var errs = new List<string>();
|
var errs = new List<string>();
|
||||||
var proc = new Process() { StartInfo = start };
|
var proc = new Process() { StartInfo = start };
|
||||||
var isCancelled = false;
|
|
||||||
|
|
||||||
proc.OutputDataReceived += (_, e) =>
|
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)
|
if (e.Data != null)
|
||||||
OnReadline(e.Data);
|
OnReadline(e.Data);
|
||||||
};
|
};
|
||||||
|
|
||||||
proc.ErrorDataReceived += (_, e) =>
|
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))
|
if (string.IsNullOrEmpty(e.Data))
|
||||||
{
|
{
|
||||||
errs.Add(string.Empty);
|
errs.Add(string.Empty);
|
||||||
|
@ -97,9 +72,25 @@ namespace SourceGit.Commands
|
||||||
errs.Add(e.Data);
|
errs.Add(e.Data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var dummy = null as Process;
|
||||||
|
var dummyProcLock = new object();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
proc.Start();
|
proc.Start();
|
||||||
|
|
||||||
|
// It not safe, please only use `CancellationToken` in readonly commands.
|
||||||
|
if (CancellationToken.CanBeCanceled)
|
||||||
|
{
|
||||||
|
dummy = proc;
|
||||||
|
CancellationToken.Register(() =>
|
||||||
|
{
|
||||||
|
lock (dummyProcLock)
|
||||||
|
{
|
||||||
|
if (dummy is { HasExited: false })
|
||||||
|
dummy.Kill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -113,15 +104,23 @@ namespace SourceGit.Commands
|
||||||
proc.BeginErrorReadLine();
|
proc.BeginErrorReadLine();
|
||||||
proc.WaitForExit();
|
proc.WaitForExit();
|
||||||
|
|
||||||
|
if (dummy != null)
|
||||||
|
{
|
||||||
|
lock (dummyProcLock)
|
||||||
|
{
|
||||||
|
dummy = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int exitCode = proc.ExitCode;
|
int exitCode = proc.ExitCode;
|
||||||
proc.Close();
|
proc.Close();
|
||||||
|
|
||||||
if (!isCancelled && exitCode != 0)
|
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
|
||||||
{
|
{
|
||||||
if (RaiseError)
|
if (RaiseError)
|
||||||
{
|
{
|
||||||
var errMsg = string.Join("\n", errs);
|
var errMsg = string.Join("\n", errs).Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(errMsg))
|
if (!string.IsNullOrEmpty(errMsg))
|
||||||
Dispatcher.UIThread.Post(() => App.RaiseException(Context, 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))
|
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
|
||||||
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
|
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
|
||||||
|
|
||||||
// Force using en_US.UTF-8 locale to avoid GCM crash
|
// Force using en_US.UTF-8 locale
|
||||||
if (OperatingSystem.IsLinux())
|
if (OperatingSystem.IsLinux())
|
||||||
start.Environment.Add("LANG", "en_US.UTF-8");
|
{
|
||||||
|
start.Environment.Add("LANG", "C");
|
||||||
// Fix macOS `PATH` env
|
start.Environment.Add("LC_ALL", "C");
|
||||||
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
|
}
|
||||||
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
|
|
||||||
|
|
||||||
// Force using this app as git editor.
|
// Force using this app as git editor.
|
||||||
switch (Editor)
|
switch (Editor)
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public partial class CompareRevisions : Command
|
public partial class CompareRevisions : Command
|
||||||
{
|
{
|
||||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
[GeneratedRegex(@"^([MADRC])\s+(.+)$")]
|
||||||
private static partial Regex REG_FORMAT();
|
private static partial Regex REG_FORMAT();
|
||||||
|
|
||||||
public CompareRevisions(string repo, string start, string end)
|
public CompareRevisions(string repo, string start, string end)
|
||||||
|
@ -18,6 +18,15 @@ namespace SourceGit.Commands
|
||||||
Args = $"diff --name-status {based} {end}";
|
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()
|
public List<Models.Change> Result()
|
||||||
{
|
{
|
||||||
Exec();
|
Exec();
|
||||||
|
|
|
@ -29,7 +29,7 @@ namespace SourceGit.Commands
|
||||||
var rs = new Dictionary<string, string>();
|
var rs = new Dictionary<string, string>();
|
||||||
if (output.IsSuccess)
|
if (output.IsSuccess)
|
||||||
{
|
{
|
||||||
var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var idx = line.IndexOf('=', StringComparison.Ordinal);
|
var idx = line.IndexOf('=', StringComparison.Ordinal);
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
Args = "status -uno --ignore-submodules=dirty --porcelain";
|
Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain";
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Result()
|
public int Result()
|
||||||
|
@ -16,7 +16,7 @@ namespace SourceGit.Commands
|
||||||
var rs = ReadToEnd();
|
var rs = ReadToEnd();
|
||||||
if (rs.IsSuccess)
|
if (rs.IsSuccess)
|
||||||
{
|
{
|
||||||
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
return lines.Length;
|
return lines.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,18 @@ namespace SourceGit.Commands
|
||||||
return;
|
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)
|
if (_result.IsBinary)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,26 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public static class ExecuteCustomAction
|
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();
|
var start = new ProcessStartInfo();
|
||||||
start.FileName = file;
|
start.FileName = file;
|
||||||
|
@ -21,14 +40,6 @@ namespace SourceGit.Commands
|
||||||
start.StandardErrorEncoding = Encoding.UTF8;
|
start.StandardErrorEncoding = Encoding.UTF8;
|
||||||
start.WorkingDirectory = repo;
|
start.WorkingDirectory = repo;
|
||||||
|
|
||||||
// Force using en_US.UTF-8 locale to avoid GCM crash
|
|
||||||
if (OperatingSystem.IsLinux())
|
|
||||||
start.Environment.Add("LANG", "en_US.UTF-8");
|
|
||||||
|
|
||||||
// Fix macOS `PATH` env
|
|
||||||
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
|
|
||||||
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
|
|
||||||
|
|
||||||
var proc = new Process() { StartInfo = start };
|
var proc = new Process() { StartInfo = start };
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
@ -53,26 +64,21 @@ namespace SourceGit.Commands
|
||||||
proc.BeginOutputReadLine();
|
proc.BeginOutputReadLine();
|
||||||
proc.BeginErrorReadLine();
|
proc.BeginErrorReadLine();
|
||||||
proc.WaitForExit();
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
|
||||||
{
|
|
||||||
App.RaiseException(repo, e.Message);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitCode = proc.ExitCode;
|
|
||||||
proc.Close();
|
proc.Close();
|
||||||
|
|
||||||
if (exitCode != 0)
|
|
||||||
{
|
|
||||||
var errMsg = builder.ToString();
|
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
|
||||||
{
|
|
||||||
App.RaiseException(repo, errMsg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public class Fetch : Command
|
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;
|
_outputHandler = outputHandler;
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
|
@ -21,9 +21,6 @@ namespace SourceGit.Commands
|
||||||
if (force)
|
if (force)
|
||||||
Args += "--force ";
|
Args += "--force ";
|
||||||
|
|
||||||
if (prune)
|
|
||||||
Args += "--prune ";
|
|
||||||
|
|
||||||
Args += remote;
|
Args += remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
namespace SourceGit.Commands
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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;
|
_service = service;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_changes = changes;
|
_changes = changes;
|
||||||
_cancelToken = cancelToken;
|
_cancelToken = cancelToken;
|
||||||
_onProgress = onProgress;
|
_onResponse = onResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Result()
|
public void Exec()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var summarybuilder = new StringBuilder();
|
_onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
|
||||||
var bodyBuilder = new StringBuilder();
|
|
||||||
|
var responseBuilder = new StringBuilder();
|
||||||
|
var summaryBuilder = new StringBuilder();
|
||||||
foreach (var change in _changes)
|
foreach (var change in _changes)
|
||||||
{
|
{
|
||||||
if (_cancelToken.IsCancellationRequested)
|
if (_cancelToken.IsCancellationRequested)
|
||||||
return "";
|
return;
|
||||||
|
|
||||||
_onProgress?.Invoke($"Analyzing {change.Path}...");
|
responseBuilder.Append("- ");
|
||||||
|
summaryBuilder.Append("- ");
|
||||||
|
|
||||||
var summary = GenerateChangeSummary(change);
|
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
|
||||||
summarybuilder.Append("- ");
|
if (rs.IsSuccess)
|
||||||
summarybuilder.Append(summary);
|
{
|
||||||
summarybuilder.Append("(file: ");
|
_service.Chat(
|
||||||
summarybuilder.Append(change.Path);
|
_service.AnalyzeDiffPrompt,
|
||||||
summarybuilder.Append(")");
|
$"Here is the `git diff` output: {rs.StdOut}",
|
||||||
summarybuilder.AppendLine();
|
_cancelToken,
|
||||||
|
update =>
|
||||||
|
{
|
||||||
|
responseBuilder.Append(update);
|
||||||
|
summaryBuilder.Append(update);
|
||||||
|
|
||||||
bodyBuilder.Append("- ");
|
_onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}");
|
||||||
bodyBuilder.Append(summary);
|
});
|
||||||
bodyBuilder.AppendLine();
|
}
|
||||||
|
|
||||||
|
responseBuilder.Append("\n");
|
||||||
|
summaryBuilder.Append("(file: ");
|
||||||
|
summaryBuilder.Append(change.Path);
|
||||||
|
summaryBuilder.Append(")\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cancelToken.IsCancellationRequested)
|
if (_cancelToken.IsCancellationRequested)
|
||||||
return "";
|
return;
|
||||||
|
|
||||||
_onProgress?.Invoke($"Generating commit message...");
|
var responseBody = responseBuilder.ToString();
|
||||||
|
var subjectBuilder = new StringBuilder();
|
||||||
var body = bodyBuilder.ToString();
|
_service.Chat(
|
||||||
var subject = GenerateSubject(summarybuilder.ToString());
|
_service.GenerateSubjectPrompt,
|
||||||
return string.Format("{0}\n\n{1}", subject, body);
|
$"Here are the summaries changes:\n{summaryBuilder}",
|
||||||
|
_cancelToken,
|
||||||
|
update =>
|
||||||
|
{
|
||||||
|
subjectBuilder.Append(update);
|
||||||
|
_onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
|
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Models.OpenAIService _service;
|
||||||
private string _repo;
|
private string _repo;
|
||||||
private List<Models.Change> _changes;
|
private List<Models.Change> _changes;
|
||||||
private CancellationToken _cancelToken;
|
private CancellationToken _cancelToken;
|
||||||
private Action<string> _onProgress;
|
private Action<string> _onResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
src/Commands/IsBareRepository.cs
Normal file
24
src/Commands/IsBareRepository.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/Commands/IsCommitSHA.cs
Normal file
17
src/Commands/IsCommitSHA.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public partial class LFS
|
public partial class LFS
|
||||||
{
|
{
|
||||||
[GeneratedRegex(@"^(.+)\s+(\w+)\s+\w+:(\d+)$")]
|
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
|
||||||
private static partial Regex REG_LOCK();
|
private static partial Regex REG_LOCK();
|
||||||
|
|
||||||
class SubCmd : Command
|
class SubCmd : Command
|
||||||
|
@ -82,7 +82,7 @@ namespace SourceGit.Commands
|
||||||
var rs = cmd.ReadToEnd();
|
var rs = cmd.ReadToEnd();
|
||||||
if (rs.IsSuccess)
|
if (rs.IsSuccess)
|
||||||
{
|
{
|
||||||
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var match = REG_LOCK().Match(line);
|
var match = REG_LOCK().Match(line);
|
||||||
|
|
|
@ -4,21 +4,20 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public class Pull : Command
|
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;
|
_outputHandler = outputHandler;
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
TraitErrorAsOutput = true;
|
TraitErrorAsOutput = true;
|
||||||
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||||
Args = "pull --verbose --progress --tags ";
|
Args = "pull --verbose --progress ";
|
||||||
|
|
||||||
if (useRebase)
|
if (useRebase)
|
||||||
Args += "--rebase ";
|
Args += "--rebase=true ";
|
||||||
|
|
||||||
if (noTags)
|
if (noTags)
|
||||||
Args += "--no-tags ";
|
Args += "--no-tags ";
|
||||||
if (prune)
|
|
||||||
Args += "--prune ";
|
|
||||||
|
|
||||||
Args += $"{remote} {branch}";
|
Args += $"{remote} {branch}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace SourceGit.Commands
|
||||||
Args += $"{remote} {local}:{remoteBranch}";
|
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;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
|
@ -36,7 +36,7 @@ namespace SourceGit.Commands
|
||||||
if (isDelete)
|
if (isDelete)
|
||||||
Args += "--delete ";
|
Args += "--delete ";
|
||||||
|
|
||||||
Args += $"{remote} refs/tags/{tag}";
|
Args += $"{remote} {refname}";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnReadline(string line)
|
protected override void OnReadline(string line)
|
||||||
|
|
|
@ -24,12 +24,23 @@ namespace SourceGit.Commands
|
||||||
if (!rs.IsSuccess)
|
if (!rs.IsSuccess)
|
||||||
return branches;
|
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)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var b = ParseLine(line);
|
var b = ParseLine(line);
|
||||||
if (b != null)
|
if (b != null)
|
||||||
|
{
|
||||||
branches.Add(b);
|
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;
|
return branches;
|
||||||
|
@ -75,6 +86,7 @@ namespace SourceGit.Commands
|
||||||
branch.Head = parts[1];
|
branch.Head = parts[1];
|
||||||
branch.IsCurrent = parts[2] == "*";
|
branch.IsCurrent = parts[2] == "*";
|
||||||
branch.Upstream = parts[3];
|
branch.Upstream = parts[3];
|
||||||
|
branch.IsUpsteamGone = false;
|
||||||
|
|
||||||
if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
|
if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
|
||||||
branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();
|
branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();
|
||||||
|
|
|
@ -9,10 +9,10 @@ namespace SourceGit.Commands
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
_commit = commit;
|
_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();
|
Exec();
|
||||||
return _lines;
|
return _lines;
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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()
|
public string Result()
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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";
|
const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null";
|
||||||
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
|
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
if (!rs.IsSuccess)
|
if (!rs.IsSuccess)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var raw = rs.StdOut.Trim();
|
var raw = rs.StdOut.Trim().ReplaceLineEndings("\n");
|
||||||
if (raw.Length <= 1)
|
if (raw.Length <= 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@
|
||||||
Signer = lines[1],
|
Signer = lines[1],
|
||||||
Key = lines[2]
|
Key = lines[2]
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,11 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public class QueryCommits : Command
|
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;
|
WorkingDirectory = repo;
|
||||||
Context = 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;
|
_findFirstMerged = needFindHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,9 +18,13 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
|
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)
|
else if (method == Models.CommitSearchMethod.ByFile)
|
||||||
{
|
{
|
||||||
|
@ -33,7 +35,7 @@ namespace SourceGit.Commands
|
||||||
var argsBuilder = new StringBuilder();
|
var argsBuilder = new StringBuilder();
|
||||||
argsBuilder.Append(search);
|
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)
|
foreach (var word in words)
|
||||||
{
|
{
|
||||||
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
|
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||||
|
@ -46,7 +48,7 @@ namespace SourceGit.Commands
|
||||||
|
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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;
|
_findFirstMerged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +124,7 @@ namespace SourceGit.Commands
|
||||||
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
|
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
|
||||||
|
|
||||||
var rs = ReadToEnd();
|
var rs = ReadToEnd();
|
||||||
var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (shas.Length == 0)
|
if (shas.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,18 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace SourceGit.Commands
|
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()} -----";
|
_boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----";
|
||||||
|
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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();
|
var rs = ReadToEnd();
|
||||||
if (!rs.IsSuccess)
|
if (!rs.IsSuccess)
|
||||||
|
@ -29,7 +29,7 @@ namespace SourceGit.Commands
|
||||||
switch (nextPartIdx)
|
switch (nextPartIdx)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
_current = new Models.CommitWithMessage();
|
_current = new Models.InteractiveCommit();
|
||||||
_current.Commit.SHA = line;
|
_current.Commit.SHA = line;
|
||||||
_commits.Add(_current);
|
_commits.Add(_current);
|
||||||
break;
|
break;
|
||||||
|
@ -52,7 +52,7 @@ namespace SourceGit.Commands
|
||||||
_current.Commit.CommitterTime = ulong.Parse(line);
|
_current.Commit.CommitterTime = ulong.Parse(line);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
|
var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal);
|
||||||
if (boundary > end)
|
if (boundary > end)
|
||||||
{
|
{
|
||||||
_current.Message = rs.StdOut.Substring(start, boundary - start - 1);
|
_current.Message = rs.StdOut.Substring(start, boundary - start - 1);
|
||||||
|
@ -88,8 +88,8 @@ namespace SourceGit.Commands
|
||||||
_current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
|
_current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Models.CommitWithMessage> _commits = new List<Models.CommitWithMessage>();
|
private List<Models.InteractiveCommit> _commits = [];
|
||||||
private Models.CommitWithMessage _current = null;
|
private Models.InteractiveCommit _current = null;
|
||||||
private string _boundary = "";
|
private string _boundary = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
26
src/Commands/QueryGitCommonDir.cs
Normal file
26
src/Commands/QueryGitCommonDir.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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()
|
public List<Models.Change> Result()
|
||||||
|
|
|
@ -20,9 +20,12 @@ namespace SourceGit.Commands
|
||||||
if (!output.IsSuccess)
|
if (!output.IsSuccess)
|
||||||
return rs;
|
return rs;
|
||||||
|
|
||||||
var lines = output.StdOut.Split('\n');
|
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
|
if (line.EndsWith("/HEAD", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (line.StartsWith("refs/heads/", StringComparison.Ordinal))
|
if (line.StartsWith("refs/heads/", StringComparison.Ordinal))
|
||||||
rs.Add(new() { Name = line.Substring("refs/heads/".Length), Type = Models.DecoratorType.LocalBranchHead });
|
rs.Add(new() { Name = line.Substring("refs/heads/".Length), Type = Models.DecoratorType.LocalBranchHead });
|
||||||
else if (line.StartsWith("refs/remotes/", StringComparison.Ordinal))
|
else if (line.StartsWith("refs/remotes/", StringComparison.Ordinal))
|
||||||
|
|
21
src/Commands/QueryRevisionByRefName.cs
Normal file
21
src/Commands/QueryRevisionByRefName.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
namespace SourceGit.Commands
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public class QueryRevisionFileNames : Command
|
public class QueryRevisionFileNames : Command
|
||||||
{
|
{
|
||||||
|
@ -9,13 +11,17 @@
|
||||||
Args = $"ls-tree -r -z --name-only {revision}";
|
Args = $"ls-tree -r -z --name-only {revision}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] Result()
|
public List<string> Result()
|
||||||
{
|
{
|
||||||
var rs = ReadToEnd();
|
var rs = ReadToEnd();
|
||||||
if (rs.IsSuccess)
|
if (!rs.IsSuccess)
|
||||||
return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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()
|
public Models.Commit Result()
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace SourceGit.Commands
|
||||||
if (rs.IsSuccess)
|
if (rs.IsSuccess)
|
||||||
{
|
{
|
||||||
var changes = new List<Models.Change>();
|
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)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var match = REG_FORMAT2().Match(line);
|
var match = REG_FORMAT2().Match(line);
|
||||||
|
|
|
@ -1,60 +1,68 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace SourceGit.Commands
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Query stash changes. Requires git >= 2.32.0
|
||||||
|
/// </summary>
|
||||||
public partial class QueryStashChanges : Command
|
public partial class QueryStashChanges : Command
|
||||||
{
|
{
|
||||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
[GeneratedRegex(@"^([MADRC])\s+(.+)$")]
|
||||||
private static partial Regex REG_FORMAT();
|
private static partial Regex REG_FORMAT();
|
||||||
|
|
||||||
public QueryStashChanges(string repo, string sha)
|
public QueryStashChanges(string repo, string stash)
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
|
Args = $"stash show -u --name-status \"{stash}\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Models.Change> Result()
|
public List<Models.Change> Result()
|
||||||
{
|
{
|
||||||
Exec();
|
var rs = ReadToEnd();
|
||||||
return _changes;
|
if (!rs.IsSuccess)
|
||||||
}
|
return [];
|
||||||
|
|
||||||
protected override void OnReadline(string line)
|
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
{
|
var outs = new List<Models.Change>();
|
||||||
var match = REG_FORMAT().Match(line);
|
foreach (var line in lines)
|
||||||
if (!match.Success)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
|
||||||
var status = match.Groups[1].Value;
|
|
||||||
|
|
||||||
switch (status[0])
|
|
||||||
{
|
{
|
||||||
case 'M':
|
var match = REG_FORMAT().Match(line);
|
||||||
change.Set(Models.ChangeState.Modified);
|
if (!match.Success)
|
||||||
_changes.Add(change);
|
continue;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace SourceGit.Commands
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
|
@ -8,7 +9,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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()
|
public List<Models.Stash> Result()
|
||||||
|
@ -26,21 +27,32 @@ namespace SourceGit.Commands
|
||||||
_stashes.Add(_current);
|
_stashes.Add(_current);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
_current.Time = ulong.Parse(line);
|
ParseParent(line);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
_current.Name = line;
|
_current.Time = ulong.Parse(line);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
_current.Name = line;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
_current.Message = line;
|
_current.Message = line;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextLineIdx++;
|
_nextLineIdx++;
|
||||||
if (_nextLineIdx > 3)
|
if (_nextLineIdx > 4)
|
||||||
_nextLineIdx = 0;
|
_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 readonly List<Models.Stash> _stashes = new List<Models.Stash>();
|
||||||
private Models.Stash _current = null;
|
private Models.Stash _current = null;
|
||||||
private int _nextLineIdx = 0;
|
private int _nextLineIdx = 0;
|
||||||
|
|
|
@ -24,11 +24,9 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
var submodules = new List<Models.Submodule>();
|
var submodules = new List<Models.Submodule>();
|
||||||
var rs = ReadToEnd();
|
var rs = ReadToEnd();
|
||||||
if (!rs.IsSuccess)
|
|
||||||
return submodules;
|
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
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)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var match = REG_FORMAT1().Match(line);
|
var match = REG_FORMAT1().Match(line);
|
||||||
|
@ -51,13 +49,13 @@ namespace SourceGit.Commands
|
||||||
|
|
||||||
if (submodules.Count > 0)
|
if (submodules.Count > 0)
|
||||||
{
|
{
|
||||||
Args = $"status -uno --porcelain -- {builder}";
|
Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
|
||||||
rs = ReadToEnd();
|
rs = ReadToEnd();
|
||||||
if (!rs.IsSuccess)
|
if (!rs.IsSuccess)
|
||||||
return submodules;
|
return submodules;
|
||||||
|
|
||||||
var dirty = new HashSet<string>();
|
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)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var match = REG_FORMAT_STATUS().Match(line);
|
var match = REG_FORMAT_STATUS().Match(line);
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace SourceGit.Commands
|
||||||
|
|
||||||
Context = repo;
|
Context = repo;
|
||||||
WorkingDirectory = 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()
|
public List<Models.Tag> Result()
|
||||||
|
@ -25,14 +25,15 @@ namespace SourceGit.Commands
|
||||||
foreach (var record in records)
|
foreach (var record in records)
|
||||||
{
|
{
|
||||||
var subs = record.Split('\0', StringSplitOptions.None);
|
var subs = record.Split('\0', StringSplitOptions.None);
|
||||||
if (subs.Length != 4)
|
if (subs.Length != 5)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var message = subs[3].Trim();
|
var message = subs[4].Trim();
|
||||||
tags.Add(new Models.Tag()
|
tags.Add(new Models.Tag()
|
||||||
{
|
{
|
||||||
Name = subs[0].Substring(10),
|
Name = subs[0].Substring(10),
|
||||||
SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
|
SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
|
||||||
|
CreatorDate = ulong.Parse(subs[3]),
|
||||||
Message = string.IsNullOrEmpty(message) ? null : message,
|
Message = string.IsNullOrEmpty(message) ? null : message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace SourceGit.Commands
|
||||||
if (!rs.IsSuccess)
|
if (!rs.IsSuccess)
|
||||||
return status;
|
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)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
if (line[0] == '>')
|
if (line[0] == '>')
|
||||||
|
|
|
@ -37,6 +37,19 @@ namespace SourceGit.Commands
|
||||||
return true;
|
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)
|
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
|
||||||
{
|
{
|
||||||
var starter = new ProcessStartInfo();
|
var starter = new ProcessStartInfo();
|
||||||
|
|
|
@ -11,72 +11,84 @@ namespace SourceGit.Commands
|
||||||
Context = repo;
|
Context = repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Push(string message)
|
public bool Push(string message, bool includeUntracked = true, bool keepIndex = false)
|
||||||
{
|
|
||||||
Args = $"stash push -m \"{message}\"";
|
|
||||||
return Exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Push(List<Models.Change> changes, string message, bool onlyStaged, bool keepIndex)
|
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.Append("stash push ");
|
builder.Append("stash push ");
|
||||||
if (onlyStaged)
|
if (includeUntracked)
|
||||||
builder.Append("--staged ");
|
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)
|
if (keepIndex)
|
||||||
builder.Append("--keep-index ");
|
builder.Append("--keep-index ");
|
||||||
builder.Append("-m \"");
|
builder.Append("-m \"");
|
||||||
builder.Append(message);
|
builder.Append(message);
|
||||||
builder.Append("\" -- ");
|
builder.Append("\" -- ");
|
||||||
|
|
||||||
if (onlyStaged)
|
foreach (var c in changes)
|
||||||
{
|
builder.Append($"\"{c.Path}\" ");
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Args = builder.ToString();
|
Args = builder.ToString();
|
||||||
return Exec();
|
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();
|
return Exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Pop(string name)
|
public bool Pop(string name)
|
||||||
{
|
{
|
||||||
Args = $"stash pop -q {name}";
|
Args = $"stash pop -q --index \"{name}\"";
|
||||||
return Exec();
|
return Exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Drop(string name)
|
public bool Drop(string name)
|
||||||
{
|
{
|
||||||
Args = $"stash drop -q {name}";
|
Args = $"stash drop -q \"{name}\"";
|
||||||
return Exec();
|
return Exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = 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()
|
public Models.Statistics Result()
|
||||||
|
|
|
@ -48,9 +48,7 @@ namespace SourceGit.Commands
|
||||||
if (remotes != null)
|
if (remotes != null)
|
||||||
{
|
{
|
||||||
foreach (var r in remotes)
|
foreach (var r in remotes)
|
||||||
{
|
new Push(repo, r.Name, $"refs/tags/{name}", true).Exec();
|
||||||
new Push(repo, r.Name, name, true).Exec();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace SourceGit.Commands
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
|
@ -20,12 +21,13 @@ namespace SourceGit.Commands
|
||||||
var last = null as Models.Worktree;
|
var last = null as Models.Worktree;
|
||||||
if (rs.IsSuccess)
|
if (rs.IsSuccess)
|
||||||
{
|
{
|
||||||
var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
if (line.StartsWith("worktree ", StringComparison.Ordinal))
|
if (line.StartsWith("worktree ", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
|
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
|
||||||
|
last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath);
|
||||||
worktrees.Add(last);
|
worktrees.Add(last);
|
||||||
}
|
}
|
||||||
else if (line.StartsWith("bare", StringComparison.Ordinal))
|
else if (line.StartsWith("bare", StringComparison.Ordinal))
|
||||||
|
@ -73,6 +75,8 @@ namespace SourceGit.Commands
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(tracking))
|
if (!string.IsNullOrEmpty(tracking))
|
||||||
Args += tracking;
|
Args += tracking;
|
||||||
|
else if (!string.IsNullOrEmpty(name) && !createNew)
|
||||||
|
Args += name;
|
||||||
|
|
||||||
_outputHandler = outputHandler;
|
_outputHandler = outputHandler;
|
||||||
return Exec();
|
return Exec();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
namespace SourceGit.Converters
|
namespace SourceGit.Converters
|
||||||
{
|
{
|
||||||
|
@ -6,5 +7,8 @@ namespace SourceGit.Converters
|
||||||
{
|
{
|
||||||
public static readonly FuncValueConverter<bool, double> ToPageTabWidth =
|
public static readonly FuncValueConverter<bool, double> ToPageTabWidth =
|
||||||
new FuncValueConverter<bool, double>(x => x ? 200 : double.NaN);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,10 @@ namespace SourceGit.Converters
|
||||||
new FuncValueConverter<int, bool>(v => v != 1);
|
new FuncValueConverter<int, bool>(v => v != 1);
|
||||||
|
|
||||||
public static readonly FuncValueConverter<int, bool> IsSubjectLengthBad =
|
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 =
|
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 =
|
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
|
||||||
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
|
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
|
|
||||||
namespace SourceGit.Converters
|
namespace SourceGit.Converters
|
||||||
{
|
{
|
||||||
public static partial class StringConverters
|
public static class StringConverters
|
||||||
{
|
{
|
||||||
public class ToLocaleConverter : IValueConverter
|
public class ToLocaleConverter : IValueConverter
|
||||||
{
|
{
|
||||||
|
@ -68,22 +67,6 @@ namespace SourceGit.Converters
|
||||||
public static readonly FuncValueConverter<string, string> ToShortSHA =
|
public static readonly FuncValueConverter<string, string> ToShortSHA =
|
||||||
new FuncValueConverter<string, string>(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v));
|
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 =
|
public static readonly FuncValueConverter<string, string> TrimRefsPrefix =
|
||||||
new FuncValueConverter<string, string>(v =>
|
new FuncValueConverter<string, string>(v =>
|
||||||
{
|
{
|
||||||
|
@ -96,9 +79,7 @@ namespace SourceGit.Converters
|
||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
|
|
||||||
[GeneratedRegex(@"^[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")]
|
public static readonly FuncValueConverter<string, bool> ContainsSpaces =
|
||||||
private static partial Regex REG_GIT_VERSION();
|
new FuncValueConverter<string, bool>(v => v != null && v.Contains(' '));
|
||||||
|
|
||||||
private static readonly Version MINIMAL_GIT_VERSION = new Version(2, 23, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,22 @@
|
||||||
{
|
{
|
||||||
public class ApplyWhiteSpaceMode
|
public class ApplyWhiteSpaceMode
|
||||||
{
|
{
|
||||||
|
public static readonly ApplyWhiteSpaceMode[] Supported =
|
||||||
|
[
|
||||||
|
new ApplyWhiteSpaceMode("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
|
||||||
|
new ApplyWhiteSpaceMode("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
|
||||||
|
new ApplyWhiteSpaceMode("Error", "Raise errors and refuses to apply the patch", "error"),
|
||||||
|
new ApplyWhiteSpaceMode("Error All", "Similar to 'error', but shows more", "error-all"),
|
||||||
|
];
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Desc { get; set; }
|
public string Desc { get; set; }
|
||||||
public string Arg { get; set; }
|
public string Arg { get; set; }
|
||||||
|
|
||||||
public ApplyWhiteSpaceMode(string n, string d, string a)
|
public ApplyWhiteSpaceMode(string n, string d, string a)
|
||||||
{
|
{
|
||||||
Name = App.Text(n);
|
Name = n;
|
||||||
Desc = App.Text(d);
|
Desc = d;
|
||||||
Arg = a;
|
Arg = a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -35,7 +35,7 @@ namespace SourceGit.Models
|
||||||
|
|
||||||
private static AvatarManager _instance = null;
|
private static AvatarManager _instance = null;
|
||||||
|
|
||||||
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")]
|
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")]
|
||||||
private static partial Regex REG_GITHUB_USER_EMAIL();
|
private static partial Regex REG_GITHUB_USER_EMAIL();
|
||||||
|
|
||||||
private object _synclock = new object();
|
private object _synclock = new object();
|
||||||
|
@ -43,6 +43,7 @@ namespace SourceGit.Models
|
||||||
private List<IAvatarHost> _avatars = new List<IAvatarHost>();
|
private List<IAvatarHost> _avatars = new List<IAvatarHost>();
|
||||||
private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
|
private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
|
||||||
private HashSet<string> _requesting = new HashSet<string>();
|
private HashSet<string> _requesting = new HashSet<string>();
|
||||||
|
private HashSet<string> _defaultAvatars = new HashSet<string>();
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
|
@ -50,8 +51,8 @@ namespace SourceGit.Models
|
||||||
if (!Directory.Exists(_storePath))
|
if (!Directory.Exists(_storePath))
|
||||||
Directory.CreateDirectory(_storePath);
|
Directory.CreateDirectory(_storePath);
|
||||||
|
|
||||||
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute));
|
LoadDefaultAvatar("noreply@github.com", "github.png");
|
||||||
_resources.Add("noreply@github.com", new Bitmap(icon));
|
LoadDefaultAvatar("unrealbot@epicgames.com", "unreal.png");
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
@ -140,7 +141,7 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
if (forceRefetch)
|
if (forceRefetch)
|
||||||
{
|
{
|
||||||
if (email.Equals("noreply@github.com", StringComparison.Ordinal))
|
if (_defaultAvatars.Contains(email))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (_resources.ContainsKey(email))
|
if (_resources.ContainsKey(email))
|
||||||
|
@ -185,11 +186,18 @@ namespace SourceGit.Models
|
||||||
return null;
|
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)
|
private string GetEmailHash(string email)
|
||||||
{
|
{
|
||||||
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
|
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
|
||||||
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
|
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered).AsSpan());
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder(hash.Length * 2);
|
||||||
foreach (var c in hash)
|
foreach (var c in hash)
|
||||||
builder.Append(c.ToString("x2"));
|
builder.Append(c.ToString("x2"));
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
|
|
|
@ -34,6 +34,7 @@ namespace SourceGit.Models
|
||||||
public string Upstream { get; set; }
|
public string Upstream { get; set; }
|
||||||
public BranchTrackStatus TrackStatus { get; set; }
|
public BranchTrackStatus TrackStatus { get; set; }
|
||||||
public string Remote { get; set; }
|
public string Remote { get; set; }
|
||||||
|
public bool IsUpsteamGone { get; set; }
|
||||||
|
|
||||||
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
|
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public enum CommitSearchMethod
|
public enum CommitSearchMethod
|
||||||
{
|
{
|
||||||
ByUser,
|
BySHA = 0,
|
||||||
|
ByAuthor,
|
||||||
|
ByCommitter,
|
||||||
ByMessage,
|
ByMessage,
|
||||||
ByFile,
|
ByFile,
|
||||||
}
|
}
|
||||||
|
@ -31,9 +33,10 @@ namespace SourceGit.Models
|
||||||
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
|
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
|
||||||
public bool HasDecorators => Decorators.Count > 0;
|
public bool HasDecorators => Decorators.Count > 0;
|
||||||
|
|
||||||
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
|
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
|
||||||
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
|
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
|
||||||
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd");
|
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 IsMerged { get; set; } = false;
|
||||||
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
|
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; } = string.Empty;
|
||||||
public string Message { get; set; } = "";
|
public List<Hyperlink> Links { get; set; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,10 +25,11 @@ namespace SourceGit.Models
|
||||||
s_penCount = colors.Count;
|
s_penCount = colors.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Path(int color)
|
public class Path(int color, bool isMerged)
|
||||||
{
|
{
|
||||||
public List<Point> Points { get; } = [];
|
public List<Point> Points { get; } = [];
|
||||||
public int Color { get; } = color;
|
public int Color { get; } = color;
|
||||||
|
public bool IsMerged { get; } = isMerged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Link
|
public class Link
|
||||||
|
@ -37,6 +38,7 @@ namespace SourceGit.Models
|
||||||
public Point Control;
|
public Point Control;
|
||||||
public Point End;
|
public Point End;
|
||||||
public int Color;
|
public int Color;
|
||||||
|
public bool IsMerged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DotType
|
public enum DotType
|
||||||
|
@ -51,6 +53,7 @@ namespace SourceGit.Models
|
||||||
public DotType Type;
|
public DotType Type;
|
||||||
public Point Center;
|
public Point Center;
|
||||||
public int Color;
|
public int Color;
|
||||||
|
public bool IsMerged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Path> Paths { get; } = [];
|
public List<Path> Paths { get; } = [];
|
||||||
|
@ -68,7 +71,7 @@ namespace SourceGit.Models
|
||||||
var unsolved = new List<PathHelper>();
|
var unsolved = new List<PathHelper>();
|
||||||
var ended = new List<PathHelper>();
|
var ended = new List<PathHelper>();
|
||||||
var offsetY = -halfHeight;
|
var offsetY = -halfHeight;
|
||||||
var colorIdx = 0;
|
var colorPicker = new ColorPicker();
|
||||||
|
|
||||||
foreach (var commit in commits)
|
foreach (var commit in commits)
|
||||||
{
|
{
|
||||||
|
@ -108,7 +111,6 @@ namespace SourceGit.Models
|
||||||
}
|
}
|
||||||
|
|
||||||
isMerged = isMerged || l.IsMerged;
|
isMerged = isMerged || l.IsMerged;
|
||||||
major.IsMerged = isMerged;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -119,28 +121,35 @@ namespace SourceGit.Models
|
||||||
|
|
||||||
// Remove ended curves from unsolved
|
// Remove ended curves from unsolved
|
||||||
foreach (var l in ended)
|
foreach (var l in ended)
|
||||||
|
{
|
||||||
|
colorPicker.Recycle(l.Path.Color);
|
||||||
unsolved.Remove(l);
|
unsolved.Remove(l);
|
||||||
|
}
|
||||||
ended.Clear();
|
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)
|
if (major == null)
|
||||||
{
|
{
|
||||||
offsetX += unitWidth;
|
offsetX += unitWidth;
|
||||||
|
|
||||||
if (commit.Parents.Count > 0)
|
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);
|
unsolved.Add(major);
|
||||||
temp.Paths.Add(major.Path);
|
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.
|
// Calculate link position of this commit.
|
||||||
var position = new Point(major?.LastX ?? offsetX, offsetY);
|
var position = new Point(major?.LastX ?? offsetX, offsetY);
|
||||||
var dotColor = major?.Path.Color ?? 0;
|
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)
|
if (commit.IsCurrentHead)
|
||||||
anchor.Type = DotType.Head;
|
anchor.Type = DotType.Head;
|
||||||
else if (commit.Parents.Count > 1)
|
else if (commit.Parents.Count > 1)
|
||||||
|
@ -158,16 +167,20 @@ namespace SourceGit.Models
|
||||||
var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal));
|
var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal));
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
{
|
{
|
||||||
// Try to change the merge state of linked graph
|
if (isMerged && !parent.IsMerged)
|
||||||
if (isMerged)
|
{
|
||||||
parent.IsMerged = true;
|
parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight);
|
||||||
|
parent.ReplaceMerged();
|
||||||
|
temp.Paths.Add(parent.Path);
|
||||||
|
}
|
||||||
|
|
||||||
temp.Links.Add(new Link
|
temp.Links.Add(new Link
|
||||||
{
|
{
|
||||||
Start = position,
|
Start = position,
|
||||||
End = new Point(parent.LastX, offsetY + halfHeight),
|
End = new Point(parent.LastX, offsetY + halfHeight),
|
||||||
Control = new Point(parent.LastX, position.Y),
|
Control = new Point(parent.LastX, position.Y),
|
||||||
Color = parent.Path.Color
|
Color = parent.Path.Color,
|
||||||
|
IsMerged = isMerged,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -175,10 +188,9 @@ namespace SourceGit.Models
|
||||||
offsetX += unitWidth;
|
offsetX += unitWidth;
|
||||||
|
|
||||||
// Create new curve for parent commit that not includes before
|
// 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);
|
unsolved.Add(l);
|
||||||
temp.Paths.Add(l.Path);
|
temp.Paths.Add(l.Path);
|
||||||
colorIdx = (colorIdx + 1) % s_penCount;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,32 +217,53 @@ namespace SourceGit.Models
|
||||||
return temp;
|
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
|
private class PathHelper
|
||||||
{
|
{
|
||||||
public Path Path { get; }
|
public Path Path { get; private set; }
|
||||||
public string Next { get; set; }
|
public string Next { get; set; }
|
||||||
public bool IsMerged { get; set; }
|
|
||||||
public double LastX { get; private set; }
|
public double LastX { get; private set; }
|
||||||
|
|
||||||
|
public bool IsMerged => Path.IsMerged;
|
||||||
|
|
||||||
public PathHelper(string next, bool isMerged, int color, Point start)
|
public PathHelper(string next, bool isMerged, int color, Point start)
|
||||||
{
|
{
|
||||||
Next = next;
|
Next = next;
|
||||||
IsMerged = isMerged;
|
|
||||||
LastX = start.X;
|
LastX = start.X;
|
||||||
_lastY = start.Y;
|
_lastY = start.Y;
|
||||||
|
|
||||||
Path = new Path(color);
|
Path = new Path(color, isMerged);
|
||||||
Path.Points.Add(start);
|
Path.Points.Add(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PathHelper(string next, bool isMerged, int color, Point start, Point to)
|
public PathHelper(string next, bool isMerged, int color, Point start, Point to)
|
||||||
{
|
{
|
||||||
Next = next;
|
Next = next;
|
||||||
IsMerged = isMerged;
|
|
||||||
LastX = to.X;
|
LastX = to.X;
|
||||||
_lastY = to.Y;
|
_lastY = to.Y;
|
||||||
|
|
||||||
Path = new Path(color);
|
Path = new Path(color, isMerged);
|
||||||
Path.Points.Add(start);
|
Path.Points.Add(start);
|
||||||
Path.Points.Add(to);
|
Path.Points.Add(to);
|
||||||
}
|
}
|
||||||
|
@ -310,6 +343,19 @@ namespace SourceGit.Models
|
||||||
_lastY = y;
|
_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)
|
private void Add(double x, double y)
|
||||||
{
|
{
|
||||||
if (_endY < y)
|
if (_endY < y)
|
||||||
|
@ -327,7 +373,6 @@ namespace SourceGit.Models
|
||||||
private static readonly List<Color> s_defaultPenColors = [
|
private static readonly List<Color> s_defaultPenColors = [
|
||||||
Colors.Orange,
|
Colors.Orange,
|
||||||
Colors.ForestGreen,
|
Colors.ForestGreen,
|
||||||
Colors.Gray,
|
|
||||||
Colors.Turquoise,
|
Colors.Turquoise,
|
||||||
Colors.Olive,
|
Colors.Olive,
|
||||||
Colors.Magenta,
|
Colors.Magenta,
|
||||||
|
|
|
@ -6,6 +6,7 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
Repository,
|
Repository,
|
||||||
Commit,
|
Commit,
|
||||||
|
Branch,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomAction : ObservableObject
|
public class CustomAction : ObservableObject
|
||||||
|
@ -34,9 +35,16 @@ namespace SourceGit.Models
|
||||||
set => SetProperty(ref _arguments, value);
|
set => SetProperty(ref _arguments, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool WaitForExit
|
||||||
|
{
|
||||||
|
get => _waitForExit;
|
||||||
|
set => SetProperty(ref _waitForExit, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string _name = string.Empty;
|
private string _name = string.Empty;
|
||||||
private CustomActionScope _scope = CustomActionScope.Repository;
|
private CustomActionScope _scope = CustomActionScope.Repository;
|
||||||
private string _executable = string.Empty;
|
private string _executable = string.Empty;
|
||||||
private string _arguments = string.Empty;
|
private string _arguments = string.Empty;
|
||||||
|
private bool _waitForExit = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
50
src/Models/DateTimeFormat.cs
Normal file
50
src/Models/DateTimeFormat.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
namespace SourceGit.Models
|
|
||||||
{
|
|
||||||
public enum DealWithLocalChanges
|
|
||||||
{
|
|
||||||
DoNothing,
|
|
||||||
StashAndReaply,
|
|
||||||
Discard,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -681,6 +681,18 @@ namespace SourceGit.Models
|
||||||
public TextDiff TextDiff { get; set; } = null;
|
public TextDiff TextDiff { get; set; } = null;
|
||||||
public LFSDiff LFSDiff { 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(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(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(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(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
|
||||||
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""),
|
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""),
|
||||||
};
|
};
|
||||||
|
|
30
src/Models/GitVersions.cs
Normal file
30
src/Models/GitVersions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,6 @@
|
||||||
{
|
{
|
||||||
public interface IRepository
|
public interface IRepository
|
||||||
{
|
{
|
||||||
string FullPath { get; set; }
|
|
||||||
string GitDir { get; set; }
|
|
||||||
|
|
||||||
void RefreshBranches();
|
void RefreshBranches();
|
||||||
void RefreshWorktrees();
|
void RefreshWorktrees();
|
||||||
void RefreshTags();
|
void RefreshTags();
|
||||||
|
|
|
@ -12,6 +12,12 @@ namespace SourceGit.Models
|
||||||
Drop,
|
Drop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class InteractiveCommit
|
||||||
|
{
|
||||||
|
public Commit Commit { get; set; } = new Commit();
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public class InteractiveRebaseJob
|
public class InteractiveRebaseJob
|
||||||
{
|
{
|
||||||
public string SHA { get; set; } = string.Empty;
|
public string SHA { get; set; } = string.Empty;
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
public static readonly MergeMode[] Supported =
|
public static readonly MergeMode[] Supported =
|
||||||
[
|
[
|
||||||
new MergeMode("Default", "Fast-forward if possible", ""),
|
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("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"),
|
new MergeMode("Don't commit", "Merge without commit", "--no-ff --no-commit"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,79 +1,99 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.ClientModel;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.RegularExpressions;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Azure.AI.OpenAI;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using OpenAI;
|
||||||
|
using OpenAI.Chat;
|
||||||
|
|
||||||
namespace SourceGit.Models
|
namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public class OpenAIChatMessage
|
public partial class OpenAIResponse
|
||||||
{
|
{
|
||||||
[JsonPropertyName("role")]
|
public OpenAIResponse(Action<string> onUpdate)
|
||||||
public string Role
|
|
||||||
{
|
{
|
||||||
get;
|
_onUpdate = onUpdate;
|
||||||
set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("content")]
|
public void Append(string text)
|
||||||
public string Content
|
|
||||||
{
|
{
|
||||||
get;
|
var buffer = text;
|
||||||
set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenAIChatChoice
|
if (_thinkTail.Length > 0)
|
||||||
{
|
{
|
||||||
[JsonPropertyName("index")]
|
_thinkTail.Append(buffer);
|
||||||
public int Index
|
buffer = _thinkTail.ToString();
|
||||||
{
|
_thinkTail.Clear();
|
||||||
get;
|
}
|
||||||
set;
|
|
||||||
|
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 void End()
|
||||||
public OpenAIChatMessage Message
|
|
||||||
{
|
{
|
||||||
get;
|
if (_thinkTail.Length > 0)
|
||||||
set;
|
{
|
||||||
}
|
OnReceive(_thinkTail.ToString());
|
||||||
}
|
_thinkTail.Clear();
|
||||||
|
}
|
||||||
public class OpenAIChatResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("choices")]
|
|
||||||
public List<OpenAIChatChoice> Choices
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
} = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenAIChatRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("model")]
|
|
||||||
public string Model
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("messages")]
|
private void OnReceive(string text)
|
||||||
public List<OpenAIChatMessage> Messages
|
|
||||||
{
|
{
|
||||||
get;
|
if (!_hasTrimmedStart)
|
||||||
set;
|
{
|
||||||
} = [];
|
text = text.TrimStart();
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return;
|
||||||
|
|
||||||
public void AddMessage(string role, string content)
|
_hasTrimmedStart = true;
|
||||||
{
|
}
|
||||||
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
|
|
||||||
|
_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
|
public class OpenAIService : ObservableObject
|
||||||
|
@ -87,7 +107,15 @@ namespace SourceGit.Models
|
||||||
public string Server
|
public string Server
|
||||||
{
|
{
|
||||||
get => _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
|
public string ApiKey
|
||||||
|
@ -102,6 +130,12 @@ namespace SourceGit.Models
|
||||||
set => SetProperty(ref _model, value);
|
set => SetProperty(ref _model, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Streaming
|
||||||
|
{
|
||||||
|
get => _streaming;
|
||||||
|
set => SetProperty(ref _streaming, value);
|
||||||
|
}
|
||||||
|
|
||||||
public string AnalyzeDiffPrompt
|
public string AnalyzeDiffPrompt
|
||||||
{
|
{
|
||||||
get => _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 };
|
var server = new Uri(_server);
|
||||||
chat.AddMessage("user", prompt);
|
var key = new ApiKeyCredential(_apiKey);
|
||||||
chat.AddMessage("user", question);
|
var client = null as ChatClient;
|
||||||
|
if (_server.Contains("openai.azure.com/", StringComparison.Ordinal))
|
||||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
|
|
||||||
if (!string.IsNullOrEmpty(ApiKey))
|
|
||||||
{
|
{
|
||||||
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
|
var azure = new AzureOpenAIClient(server, key);
|
||||||
client.DefaultRequestHeaders.Add("api-key", ApiKey);
|
client = azure.GetChatClient(_model);
|
||||||
else
|
}
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
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
|
try
|
||||||
{
|
{
|
||||||
var task = client.PostAsync(Server, req, cancellation);
|
var rsp = new OpenAIResponse(onUpdate);
|
||||||
task.Wait(cancellation);
|
|
||||||
|
|
||||||
var rsp = task.Result;
|
if (_streaming)
|
||||||
var reader = rsp.Content.ReadAsStringAsync(cancellation);
|
|
||||||
reader.Wait(cancellation);
|
|
||||||
|
|
||||||
var body = reader.Result;
|
|
||||||
if (!rsp.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
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
|
catch
|
||||||
{
|
{
|
||||||
if (cancellation.IsCancellationRequested)
|
if (!cancellation.IsCancellationRequested)
|
||||||
return null;
|
throw;
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +236,7 @@ namespace SourceGit.Models
|
||||||
private string _server;
|
private string _server;
|
||||||
private string _apiKey;
|
private string _apiKey;
|
||||||
private string _model;
|
private string _model;
|
||||||
|
private bool _streaming = true;
|
||||||
private string _analyzeDiffPrompt;
|
private string _analyzeDiffPrompt;
|
||||||
private string _generateSubjectPrompt;
|
private string _generateSubjectPrompt;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?$")]
|
[GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")]
|
||||||
private static partial Regex REG_HTTPS();
|
private static partial Regex REG_HTTPS();
|
||||||
[GeneratedRegex(@"^[\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();
|
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();
|
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 partial Regex REG_TO_VISIT_URL_CAPTURE();
|
||||||
|
|
||||||
private static readonly Regex[] URL_FORMATS = [
|
private static readonly Regex[] URL_FORMATS = [
|
||||||
|
|
|
@ -14,13 +14,43 @@ namespace SourceGit.Models
|
||||||
set;
|
set;
|
||||||
} = string.Empty;
|
} = string.Empty;
|
||||||
|
|
||||||
public DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch
|
public bool EnableReflog
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
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;
|
get;
|
||||||
set;
|
set;
|
||||||
|
@ -32,12 +62,6 @@ namespace SourceGit.Models
|
||||||
set;
|
set;
|
||||||
} = false;
|
} = false;
|
||||||
|
|
||||||
public DealWithLocalChanges DealWithLocalChangesOnPull
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
} = DealWithLocalChanges.DoNothing;
|
|
||||||
|
|
||||||
public bool PreferRebaseInsteadOfMerge
|
public bool PreferRebaseInsteadOfMerge
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
|
@ -68,11 +92,17 @@ namespace SourceGit.Models
|
||||||
set;
|
set;
|
||||||
} = false;
|
} = false;
|
||||||
|
|
||||||
public DealWithLocalChanges DealWithLocalChangesOnCreateBranch
|
public bool PushToRemoteWhenCreateTag
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = DealWithLocalChanges.DoNothing;
|
} = true;
|
||||||
|
|
||||||
|
public bool PushToRemoteWhenDeleteTag
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = false;
|
||||||
|
|
||||||
public bool CheckoutBranchOnCreateBranch
|
public bool CheckoutBranchOnCreateBranch
|
||||||
{
|
{
|
||||||
|
@ -84,31 +114,31 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = new AvaloniaList<Filter>();
|
} = [];
|
||||||
|
|
||||||
public AvaloniaList<CommitTemplate> CommitTemplates
|
public AvaloniaList<CommitTemplate> CommitTemplates
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = new AvaloniaList<CommitTemplate>();
|
} = [];
|
||||||
|
|
||||||
public AvaloniaList<string> CommitMessages
|
public AvaloniaList<string> CommitMessages
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = new AvaloniaList<string>();
|
} = [];
|
||||||
|
|
||||||
public AvaloniaList<IssueTrackerRule> IssueTrackerRules
|
public AvaloniaList<IssueTrackerRule> IssueTrackerRules
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = new AvaloniaList<IssueTrackerRule>();
|
} = [];
|
||||||
|
|
||||||
public AvaloniaList<CustomAction> CustomActions
|
public AvaloniaList<CustomAction> CustomActions
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = new AvaloniaList<CustomAction>();
|
} = [];
|
||||||
|
|
||||||
public bool EnableAutoFetch
|
public bool EnableAutoFetch
|
||||||
{
|
{
|
||||||
|
@ -146,12 +176,54 @@ namespace SourceGit.Models
|
||||||
set;
|
set;
|
||||||
} = false;
|
} = false;
|
||||||
|
|
||||||
|
public bool AutoRestoreAfterStash
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = false;
|
||||||
|
|
||||||
public string PreferedOpenAIService
|
public string PreferedOpenAIService
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
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()
|
public Dictionary<string, FilterMode> CollectHistoriesFilters()
|
||||||
{
|
{
|
||||||
var map = new Dictionary<string, FilterMode>();
|
var map = new Dictionary<string, FilterMode>();
|
||||||
|
@ -378,65 +450,13 @@ namespace SourceGit.Models
|
||||||
CommitMessages.Insert(0, message);
|
CommitMessages.Insert(0, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IssueTrackerRule AddNewIssueTracker()
|
public IssueTrackerRule AddIssueTracker(string name, string regex, string url)
|
||||||
{
|
{
|
||||||
var rule = new IssueTrackerRule()
|
var rule = new IssueTrackerRule()
|
||||||
{
|
{
|
||||||
Name = "New Issue Tracker",
|
Name = name,
|
||||||
RegexString = "#(\\d+)",
|
RegexString = regex,
|
||||||
URLTemplate = "https://xxx/$1",
|
URLTemplate = url,
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
IssueTrackerRules.Add(rule);
|
IssueTrackerRules.Add(rule);
|
||||||
|
@ -451,11 +471,7 @@ namespace SourceGit.Models
|
||||||
|
|
||||||
public CustomAction AddNewCustomAction()
|
public CustomAction AddNewCustomAction()
|
||||||
{
|
{
|
||||||
var act = new CustomAction()
|
var act = new CustomAction() { Name = "Unnamed Action" };
|
||||||
{
|
|
||||||
Name = "Unnamed Custom Action",
|
|
||||||
};
|
|
||||||
|
|
||||||
CustomActions.Add(act);
|
CustomActions.Add(act);
|
||||||
return act;
|
return act;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,6 @@ namespace SourceGit.Models
|
||||||
public class RevisionSubmodule
|
public class RevisionSubmodule
|
||||||
{
|
{
|
||||||
public Commit Commit { get; set; } = null;
|
public Commit Commit { get; set; } = null;
|
||||||
public string FullMessage { get; set; } = string.Empty;
|
public CommitFullMessage FullMessage { get; set; } = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Reflection;
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace SourceGit.Models
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -41,6 +41,8 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
new ShellOrTerminal("mac-terminal", "Terminal", ""),
|
new ShellOrTerminal("mac-terminal", "Terminal", ""),
|
||||||
new ShellOrTerminal("iterm2", "iTerm", ""),
|
new ShellOrTerminal("iterm2", "iTerm", ""),
|
||||||
|
new ShellOrTerminal("warp", "Warp", ""),
|
||||||
|
new ShellOrTerminal("ghostty", "Ghostty", "")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -55,6 +57,7 @@ namespace SourceGit.Models
|
||||||
new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"),
|
new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"),
|
||||||
new ShellOrTerminal("foot", "Foot", "foot"),
|
new ShellOrTerminal("foot", "Foot", "foot"),
|
||||||
new ShellOrTerminal("wezterm", "WezTerm", "wezterm"),
|
new ShellOrTerminal("wezterm", "WezTerm", "wezterm"),
|
||||||
|
new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"),
|
||||||
new ShellOrTerminal("custom", "Custom", ""),
|
new ShellOrTerminal("custom", "Custom", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace SourceGit.Models
|
namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
|
@ -6,9 +7,10 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public string SHA { get; set; } = "";
|
public string SHA { get; set; } = "";
|
||||||
|
public List<string> Parents { get; set; } = [];
|
||||||
public ulong Time { get; set; } = 0;
|
public ulong Time { get; set; } = 0;
|
||||||
public string Message { get; set; } = "";
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,18 @@
|
||||||
|
|
||||||
namespace SourceGit.Models
|
namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
|
public enum TagSortMode
|
||||||
|
{
|
||||||
|
CreatorDate = 0,
|
||||||
|
NameInAscending,
|
||||||
|
NameInDescending,
|
||||||
|
}
|
||||||
|
|
||||||
public class Tag : ObservableObject
|
public class Tag : ObservableObject
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string SHA { 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 string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
public FilterMode FilterMode
|
public FilterMode FilterMode
|
||||||
|
|
|
@ -313,7 +313,7 @@ namespace SourceGit.Models
|
||||||
|
|
||||||
private static bool IsNameChar(char c)
|
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
|
// (?) notice or log if variable is not found
|
||||||
|
|
|
@ -21,10 +21,11 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
private static readonly ExtraGrammar[] s_extraGrammars =
|
private static readonly ExtraGrammar[] s_extraGrammars =
|
||||||
[
|
[
|
||||||
new ExtraGrammar("source.toml", ".toml", "toml.json"),
|
new ExtraGrammar("source.toml", [".toml"], "toml.json"),
|
||||||
new ExtraGrammar("source.kotlin", ".kotlin", "kotlin.json"),
|
new ExtraGrammar("source.kotlin", [".kotlin", ".kt", ".kts"], "kotlin.json"),
|
||||||
new ExtraGrammar("source.hx", ".hx", "haxe.json"),
|
new ExtraGrammar("source.hx", [".hx"], "haxe.json"),
|
||||||
new ExtraGrammar("source.hxml", ".hxml", "hxml.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)
|
public static string GetScope(string file, RegistryOptions reg)
|
||||||
|
@ -36,13 +37,14 @@ namespace SourceGit.Models
|
||||||
extension = ".xml";
|
extension = ".xml";
|
||||||
else if (extension == ".command")
|
else if (extension == ".command")
|
||||||
extension = ".sh";
|
extension = ".sh";
|
||||||
else if (extension == ".kt" || extension == ".kts")
|
|
||||||
extension = ".kotlin";
|
|
||||||
|
|
||||||
foreach (var grammar in s_extraGrammars)
|
foreach (var grammar in s_extraGrammars)
|
||||||
{
|
{
|
||||||
if (grammar.Extension.Equals(extension, StringComparison.OrdinalIgnoreCase))
|
foreach (var ext in grammar.Extensions)
|
||||||
return grammar.Scope;
|
{
|
||||||
|
if (ext.Equals(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return grammar.Scope;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reg.GetScopeByExtension(extension);
|
return reg.GetScopeByExtension(extension);
|
||||||
|
@ -71,10 +73,10 @@ namespace SourceGit.Models
|
||||||
return reg.GetGrammar(scopeName);
|
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 Scope = Scope;
|
||||||
public readonly string Extension = Extension;
|
public readonly List<string> Extensions = Extensions;
|
||||||
public readonly string File = File;
|
public readonly string File = File;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,11 @@ namespace SourceGit.Models
|
||||||
return _caches.GetOrAdd(data, key => new User(key));
|
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 static ConcurrentDictionary<string, User> _caches = new ConcurrentDictionary<string, User>();
|
||||||
private readonly int _hash;
|
private readonly int _hash;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,12 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public class Watcher : IDisposable
|
public class Watcher : IDisposable
|
||||||
{
|
{
|
||||||
public Watcher(IRepository repo)
|
public Watcher(IRepository repo, string fullpath, string gitDir)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
|
|
||||||
_wcWatcher = new FileSystemWatcher();
|
_wcWatcher = new FileSystemWatcher();
|
||||||
_wcWatcher.Path = _repo.FullPath;
|
_wcWatcher.Path = fullpath;
|
||||||
_wcWatcher.Filter = "*";
|
_wcWatcher.Filter = "*";
|
||||||
_wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime;
|
_wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime;
|
||||||
_wcWatcher.IncludeSubdirectories = true;
|
_wcWatcher.IncludeSubdirectories = true;
|
||||||
|
@ -23,15 +23,8 @@ namespace SourceGit.Models
|
||||||
_wcWatcher.Deleted += OnWorkingCopyChanged;
|
_wcWatcher.Deleted += OnWorkingCopyChanged;
|
||||||
_wcWatcher.EnableRaisingEvents = true;
|
_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 = new FileSystemWatcher();
|
||||||
_repoWatcher.Path = repoWatchDir;
|
_repoWatcher.Path = gitDir;
|
||||||
_repoWatcher.Filter = "*";
|
_repoWatcher.Filter = "*";
|
||||||
_repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;
|
_repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;
|
||||||
_repoWatcher.IncludeSubdirectories = true;
|
_repoWatcher.IncludeSubdirectories = true;
|
||||||
|
@ -72,6 +65,11 @@ namespace SourceGit.Models
|
||||||
_updateBranch = DateTime.Now.ToFileTime() - 1;
|
_updateBranch = DateTime.Now.ToFileTime() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void MarkTagDirtyManually()
|
||||||
|
{
|
||||||
|
_updateTags = DateTime.Now.ToFileTime() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
public void MarkWorkingCopyDirtyManually()
|
public void MarkWorkingCopyDirtyManually()
|
||||||
{
|
{
|
||||||
_updateWC = DateTime.Now.ToFileTime() - 1;
|
_updateWC = DateTime.Now.ToFileTime() - 1;
|
||||||
|
@ -109,6 +107,7 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
_updateBranch = 0;
|
_updateBranch = 0;
|
||||||
_updateWC = 0;
|
_updateWC = 0;
|
||||||
|
_updateSubmodules = 0;
|
||||||
|
|
||||||
if (_updateTags > 0)
|
if (_updateTags > 0)
|
||||||
{
|
{
|
||||||
|
@ -119,6 +118,7 @@ namespace SourceGit.Models
|
||||||
Task.Run(_repo.RefreshBranches);
|
Task.Run(_repo.RefreshBranches);
|
||||||
Task.Run(_repo.RefreshCommits);
|
Task.Run(_repo.RefreshCommits);
|
||||||
Task.Run(_repo.RefreshWorkingCopyChanges);
|
Task.Run(_repo.RefreshWorkingCopyChanges);
|
||||||
|
Task.Run(_repo.RefreshSubmodules);
|
||||||
Task.Run(_repo.RefreshWorktrees);
|
Task.Run(_repo.RefreshWorktrees);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,20 +131,20 @@ namespace SourceGit.Models
|
||||||
if (_updateSubmodules > 0 && now > _updateSubmodules)
|
if (_updateSubmodules > 0 && now > _updateSubmodules)
|
||||||
{
|
{
|
||||||
_updateSubmodules = 0;
|
_updateSubmodules = 0;
|
||||||
_repo.RefreshSubmodules();
|
Task.Run(_repo.RefreshSubmodules);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_updateStashes > 0 && now > _updateStashes)
|
if (_updateStashes > 0 && now > _updateStashes)
|
||||||
{
|
{
|
||||||
_updateStashes = 0;
|
_updateStashes = 0;
|
||||||
_repo.RefreshStashes();
|
Task.Run(_repo.RefreshStashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_updateTags > 0 && now > _updateTags)
|
if (_updateTags > 0 && now > _updateTags)
|
||||||
{
|
{
|
||||||
_updateTags = 0;
|
_updateTags = 0;
|
||||||
_repo.RefreshTags();
|
Task.Run(_repo.RefreshTags);
|
||||||
_repo.RefreshCommits();
|
Task.Run(_repo.RefreshCommits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,12 +173,6 @@ namespace SourceGit.Models
|
||||||
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
|
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
|
_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))
|
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))
|
if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (_submodules)
|
lock (_lockSubmodule)
|
||||||
{
|
{
|
||||||
foreach (var submodule in _submodules)
|
foreach (var submodule in _submodules)
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,6 +6,7 @@ namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public string Branch { get; set; } = string.Empty;
|
public string Branch { get; set; } = string.Empty;
|
||||||
public string FullPath { 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 string Head { get; set; } = string.Empty;
|
||||||
public bool IsBare { get; set; } = false;
|
public bool IsBare { get; set; } = false;
|
||||||
public bool IsDetached { get; set; } = false;
|
public bool IsDetached { get; set; } = false;
|
||||||
|
@ -21,15 +22,15 @@ namespace SourceGit.Models
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (IsDetached)
|
if (IsDetached)
|
||||||
return $"(deteched HEAD at {Head.Substring(10)})";
|
return $"deteched HEAD at {Head.Substring(10)}";
|
||||||
|
|
||||||
if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
|
if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
|
||||||
return $"({Branch.Substring(11)})";
|
return Branch.Substring(11);
|
||||||
|
|
||||||
if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
|
if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
|
||||||
return $"({Branch.Substring(13)})";
|
return Branch.Substring(13);
|
||||||
|
|
||||||
return $"({Branch})";
|
return Branch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,13 +65,16 @@ namespace SourceGit.Native
|
||||||
{
|
{
|
||||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
var cwd = string.IsNullOrEmpty(workdir) ? home : workdir;
|
var cwd = string.IsNullOrEmpty(workdir) ? home : workdir;
|
||||||
|
var terminal = OS.ShellOrTerminal;
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo();
|
var startInfo = new ProcessStartInfo();
|
||||||
startInfo.WorkingDirectory = cwd;
|
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}\"";
|
startInfo.Arguments = $"start --cwd \"{cwd}\"";
|
||||||
|
else if (terminal.EndsWith("ptyxis", StringComparison.OrdinalIgnoreCase))
|
||||||
|
startInfo.Arguments = $"--new-window --working-directory=\"{cwd}\"";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,14 +18,33 @@ namespace SourceGit.Native
|
||||||
DisableDefaultApplicationMenuItems = true,
|
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");
|
var customPathFile = Path.Combine(OS.DataDir, "PATH");
|
||||||
if (File.Exists(customPathFile))
|
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()
|
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)
|
public string FindTerminal(Models.ShellOrTerminal shell)
|
||||||
|
@ -36,6 +55,10 @@ namespace SourceGit.Native
|
||||||
return "Terminal";
|
return "Terminal";
|
||||||
case "iterm2":
|
case "iterm2":
|
||||||
return "iTerm";
|
return "iTerm";
|
||||||
|
case "warp":
|
||||||
|
return "Warp";
|
||||||
|
case "ghostty":
|
||||||
|
return "Ghostty";
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
125
src/Native/OS.cs
125
src/Native/OS.cs
|
@ -1,12 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
|
||||||
namespace SourceGit.Native
|
namespace SourceGit.Native
|
||||||
{
|
{
|
||||||
public static class OS
|
public static partial class OS
|
||||||
{
|
{
|
||||||
public interface IBackend
|
public interface IBackend
|
||||||
{
|
{
|
||||||
|
@ -22,11 +25,48 @@ namespace SourceGit.Native
|
||||||
void OpenWithDefaultEditor(string file);
|
void OpenWithDefaultEditor(string file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DataDir { get; private set; } = string.Empty;
|
public static string DataDir
|
||||||
public static string GitExecutable { get; set; } = string.Empty;
|
{
|
||||||
public static string ShellOrTerminal { get; set; } = string.Empty;
|
get;
|
||||||
public static List<Models.ExternalTool> ExternalTools { get; set; } = [];
|
private set;
|
||||||
public static string CustomPathEnv { get; set; } = string.Empty;
|
} = 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()
|
static OS()
|
||||||
{
|
{
|
||||||
|
@ -55,6 +95,17 @@ namespace SourceGit.Native
|
||||||
|
|
||||||
public static void SetupDataDir()
|
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);
|
var osAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
if (string.IsNullOrEmpty(osAppDataDir))
|
if (string.IsNullOrEmpty(osAppDataDir))
|
||||||
DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit");
|
DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit");
|
||||||
|
@ -111,6 +162,68 @@ namespace SourceGit.Native
|
||||||
_backend.OpenWithDefaultEditor(file);
|
_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 IBackend _backend = null;
|
||||||
|
private static string _gitExecutable = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ using System.Text;
|
||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
namespace SourceGit.Native
|
namespace SourceGit.Native
|
||||||
{
|
{
|
||||||
|
@ -26,9 +27,21 @@ namespace SourceGit.Native
|
||||||
internal string szCSDVersion;
|
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);
|
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)]
|
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||||
private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
|
private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
|
||||||
|
|
||||||
|
@ -140,7 +153,7 @@ namespace SourceGit.Native
|
||||||
|
|
||||||
public void OpenBrowser(string url)
|
public void OpenBrowser(string url)
|
||||||
{
|
{
|
||||||
var info = new ProcessStartInfo("cmd", $"/c start {url}");
|
var info = new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\"");
|
||||||
info.CreateNoWindow = true;
|
info.CreateNoWindow = true;
|
||||||
Process.Start(info);
|
Process.Start(info);
|
||||||
}
|
}
|
||||||
|
@ -202,10 +215,17 @@ namespace SourceGit.Native
|
||||||
|
|
||||||
private void FixWindowFrameOnWin10(Window w)
|
private void FixWindowFrameOnWin10(Window w)
|
||||||
{
|
{
|
||||||
if (w.WindowState == WindowState.Maximized || w.WindowState == WindowState.FullScreen)
|
// Schedule the DWM frame extension to run in the next render frame
|
||||||
w.SystemDecorations = SystemDecorations.Full;
|
// to ensure proper timing with the window initialization sequence
|
||||||
else if (w.WindowState == WindowState.Normal)
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
w.SystemDecorations = SystemDecorations.BorderOnly;
|
{
|
||||||
|
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
|
#region EXTERNAL_EDITOR_FINDER
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{
|
{
|
||||||
"information_for_contributors": [
|
"information_for_contributors": [
|
||||||
"This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/haxe.tmLanguage",
|
"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": [
|
"fileTypes": [
|
||||||
"hx",
|
"hx",
|
||||||
|
@ -2485,4 +2487,4 @@
|
||||||
"name": "variable.other.hx"
|
"name": "variable.other.hx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{
|
{
|
||||||
"information_for_contributors": [
|
"information_for_contributors": [
|
||||||
"This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/hxml.tmLanguage",
|
"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": [
|
"fileTypes": [
|
||||||
"hxml"
|
"hxml"
|
||||||
|
@ -67,4 +69,4 @@
|
||||||
],
|
],
|
||||||
"scopeName": "source.hxml",
|
"scopeName": "source.hxml",
|
||||||
"uuid": "CB1B853A-C4C8-42C3-BA70-1B1605BE51C1"
|
"uuid": "CB1B853A-C4C8-42C3-BA70-1B1605BE51C1"
|
||||||
}
|
}
|
||||||
|
|
100
src/Resources/Grammars/jsp.json
Normal file
100
src/Resources/Grammars/jsp.json
Normal 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
Loading…
Add table
Add a link
Reference in a new issue