mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-06-22 02:45: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
|
||||
runtime: osx-arm64
|
||||
- name : Linux
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-latest
|
||||
runtime: linux-x64
|
||||
container: ubuntu:20.04
|
||||
- name : Linux (arm64)
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-latest
|
||||
runtime: linux-arm64
|
||||
container: ubuntu:20.04
|
||||
name: Build ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container || '' }}
|
||||
steps:
|
||||
- name: Install common CLI tools
|
||||
if: ${{ startsWith(matrix.runtime, 'linux-') }}
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
apt-get update
|
||||
apt-get install -y sudo
|
||||
sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
|
@ -47,7 +58,7 @@ jobs:
|
|||
if: ${{ matrix.runtime == 'linux-arm64' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
|
||||
sudo apt-get install -y llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
|
||||
- name: Build
|
||||
run: dotnet build -c Release
|
||||
- name: Publish
|
||||
|
|
22
.github/workflows/package.yml
vendored
22
.github/workflows/package.yml
vendored
|
@ -7,12 +7,12 @@ on:
|
|||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
windows-portable:
|
||||
name: Package portable Windows app
|
||||
runs-on: ubuntu-latest
|
||||
windows:
|
||||
name: Package Windows
|
||||
runs-on: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
runtime: [win-x64, win-arm64]
|
||||
runtime: [ win-x64, win-arm64 ]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
@ -22,10 +22,11 @@ jobs:
|
|||
name: sourcegit.${{ matrix.runtime }}
|
||||
path: build/SourceGit
|
||||
- name: Package
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
RUNTIME: ${{ matrix.runtime }}
|
||||
run: ./build/scripts/package.windows-portable.sh
|
||||
run: ./build/scripts/package.windows.sh
|
||||
- name: Upload package artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
@ -36,7 +37,7 @@ jobs:
|
|||
with:
|
||||
name: sourcegit.${{ matrix.runtime }}
|
||||
osx-app:
|
||||
name: Package OSX app
|
||||
name: Package macOS
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
@ -69,6 +70,7 @@ jobs:
|
|||
linux:
|
||||
name: Package Linux
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
strategy:
|
||||
matrix:
|
||||
runtime: [linux-x64, linux-arm64]
|
||||
|
@ -77,9 +79,10 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: Download package dependencies
|
||||
run: |
|
||||
sudo add-apt-repository universe
|
||||
sudo apt-get update
|
||||
sudo apt-get install desktop-file-utils rpm libfuse2
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
apt-get update
|
||||
apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
@ -89,6 +92,7 @@ jobs:
|
|||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
RUNTIME: ${{ matrix.runtime }}
|
||||
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||
run: |
|
||||
mkdir build/SourceGit
|
||||
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
|
||||
|
|
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
|
||||
with:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
publish-packages:
|
||||
needs: [package, version]
|
||||
name: Publish Packages
|
||||
uses: ./.github/workflows/publish-packages.yml
|
||||
secrets:
|
||||
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
|
||||
release:
|
||||
needs: [package, version]
|
||||
name: Release
|
||||
|
@ -44,7 +38,7 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
run: gh release create "$TAG" -t "Release $VERSION" --notes-from-tag
|
||||
run: gh release create "$TAG" -t "$VERSION" --notes-from-tag
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2024 sourcegit
|
||||
Copyright (c) 2025 sourcegit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
|
85
README.md
85
README.md
|
@ -18,9 +18,9 @@
|
|||
* Supports SSH access with each remote
|
||||
* GIT commands with GUI
|
||||
* Clone/Fetch/Pull/Push...
|
||||
* Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
|
||||
* Amend/Reword
|
||||
* Interactive rebase (Basic)
|
||||
* Merge/Rebase/Reset/Revert/Cherry-pick...
|
||||
* Amend/Reword/Squash
|
||||
* Interactive rebase
|
||||
* Branches
|
||||
* Remotes
|
||||
* Tags
|
||||
|
@ -40,6 +40,7 @@
|
|||
* Git LFS
|
||||
* Issue Link
|
||||
* Workspace
|
||||
* Custom Action
|
||||
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
|
||||
|
||||
> [!WARNING]
|
||||
|
@ -47,7 +48,7 @@
|
|||
|
||||
## Translation Status
|
||||
|
||||
[](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
|
||||
|
||||
|
@ -59,12 +60,13 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD
|
|||
|
||||
| OS | PATH |
|
||||
|---------|-----------------------------------------------------|
|
||||
| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` |
|
||||
| Windows | `%APPDATA%\SourceGit` |
|
||||
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
|
||||
| macOS | `${HOME}/Library/Application Support/SourceGit` |
|
||||
|
||||
> [!TIP]
|
||||
> You can open the app data dir from the main menu.
|
||||
> * You can open this data storage directory from the main menu `Open Data Storage Directory`.
|
||||
> * You can create a `data` folder next to the `SourceGit` executable to force this app to store data (user settings, downloaded avatars and crash logs) into it (Portable-Mode). Only works on Windows.
|
||||
|
||||
For **Windows** users:
|
||||
|
||||
|
@ -75,12 +77,12 @@ For **Windows** users:
|
|||
```
|
||||
> [!NOTE]
|
||||
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
|
||||
* You can install the latest stable by `scoope` with follow commands:
|
||||
* You can install the latest stable by `scoop` with follow commands:
|
||||
```shell
|
||||
scoop bucket add extras
|
||||
scoop install sourcegit
|
||||
```
|
||||
* Portable versions can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest)
|
||||
* Pre-built binaries can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest)
|
||||
|
||||
For **macOS** users:
|
||||
|
||||
|
@ -98,49 +100,45 @@ For **macOS** users:
|
|||
|
||||
For **Linux** users:
|
||||
|
||||
* For Debian/Ubuntu based distributions, you can add the `sourcegit` repository by following:
|
||||
You may need to install curl and/or gpg first, if you're on a very minimal host:
|
||||
* Thanks [@aikawayataro](https://github.com/aikawayataro) for providing `rpm` and `deb` repositories, hosted on [Codeberg](https://codeberg.org/yataro/-/packages).
|
||||
|
||||
`deb` how to:
|
||||
```shell
|
||||
apt update && apt install curl gpg -y
|
||||
curl https://codeberg.org/api/packages/yataro/debian/repository.key | sudo tee /etc/apt/keyrings/sourcegit.asc
|
||||
echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc, arch=amd64,arm64] https://codeberg.org/api/packages/yataro/debian generic main" | sudo tee /etc/apt/sources.list.d/sourcegit.list
|
||||
sudo apt update
|
||||
sudo apt install sourcegit
|
||||
```
|
||||
Install the registry signing key:
|
||||
|
||||
`rpm` how to:
|
||||
```shell
|
||||
curl -fsSL "https://packages.buildkite.com/sourcegit/sourcegit-deb/gpgkey" | gpg --dearmor -o /etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg
|
||||
curl https://codeberg.org/api/packages/yataro/rpm.repo | sed -e 's/gpgcheck=1/gpgcheck=0/' > sourcegit.repo
|
||||
|
||||
# Fedora 41 and newer
|
||||
sudo dnf config-manager addrepo --from-repofile=./sourcegit.repo
|
||||
# Fedora 40 and earlier
|
||||
sudo dnf config-manager --add-repo ./sourcegit.repo
|
||||
|
||||
sudo dnf install sourcegit
|
||||
```
|
||||
Configure the source:
|
||||
```shell
|
||||
echo -e "deb [signed-by=/etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg] https://packages.buildkite.com/sourcegit/sourcegit-deb/any/ any main\ndeb-src [signed-by=/etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg] https://packages.buildkite.com/sourcegit/sourcegit-deb/any/ any main" > /etc/apt/sources.list.d/buildkite-sourcegit-sourcegit-deb.list
|
||||
```
|
||||
Update your local repository and install the package:
|
||||
```shell
|
||||
apt update && apt install sourcegit
|
||||
```
|
||||
* For RHEL/Fedora based distributions, you can add the `sourcegit` repository by following:
|
||||
Configure the source:
|
||||
```shell
|
||||
sudo sh -c 'echo -e "[sourcegit-rpm]\nname=sourcegit-rpm\nbaseurl=https://packages.buildkite.com/sourcegit/sourcegit-rpm/rpm_any/rpm_any/\$basearch\nenabled=1\nrepo_gpgcheck=1\ngpgcheck=0\ngpgkey=https://packages.buildkite.com/sourcegit/sourcegit-rpm/gpgkey\npriority=1"' > /etc/yum.repos.d/sourcegit-rpm.repo
|
||||
```
|
||||
Install the package with this command:
|
||||
```shell
|
||||
sudo dnf install -y sourcegit
|
||||
```
|
||||
* `Appimage` files can be found on [AppimageHub](https://appimage.github.io/SourceGit/)
|
||||
* `xdg-open` must be installed to support open native file manager.
|
||||
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux.
|
||||
|
||||
If your distribution isn't using `dnf`, please refer to the documentation of your distribution on how to add an `rpm` repository.
|
||||
* `AppImage` files can be found on [AppImage hub](https://appimage.github.io/SourceGit/), `xdg-open` (`xdg-utils`) must be installed to support open native file manager.
|
||||
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your Linux.
|
||||
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
|
||||
* If you can NOT type accented characters, such as `ê`, `ó`, try to set the environment variable `AVALONIA_IM_MODULE` to `none`.
|
||||
|
||||
## OpenAI
|
||||
|
||||
This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window.
|
||||
This software supports using OpenAI or other AI service that has an OpenAI compatible HTTP API to generate commit message. You need configurate the service in `Preference` window.
|
||||
|
||||
For `OpenAI`:
|
||||
|
||||
* `Server` must be `https://api.openai.com/v1/chat/completions`
|
||||
* `Server` must be `https://api.openai.com/v1`
|
||||
|
||||
For other AI service:
|
||||
|
||||
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate`
|
||||
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate`
|
||||
* The `API Key` is optional that depends on the service
|
||||
|
||||
## External Tools
|
||||
|
@ -159,7 +157,7 @@ This app supports open repository in external tools listed in the table below.
|
|||
|
||||
> [!NOTE]
|
||||
> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app.
|
||||
> To solve this problem you can add a file named `external_editors.json` in app data dir and provide the path directly. For example:
|
||||
> To solve this problem you can add a file named `external_editors.json` in app data storage directory and provide the path directly. For example:
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
|
@ -189,6 +187,19 @@ This app supports open repository in external tools listed in the table below.
|
|||
|
||||
Everyone is welcome to submit a PR. Please make sure your PR is based on the latest `develop` branch and the target branch of PR is `develop`.
|
||||
|
||||
In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download):
|
||||
|
||||
```sh
|
||||
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
|
||||
dotnet restore
|
||||
dotnet build
|
||||
dotnet run --project src/SourceGit.csproj
|
||||
```
|
||||
|
||||
Thanks to all the people who contribute.
|
||||
|
||||
[](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\release.yml = .github\workflows\release.yml
|
||||
.github\workflows\localization-check.yml = .github\workflows\localization-check.yml
|
||||
.github\workflows\publish-packages.yml = .github\workflows\publish-packages.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}"
|
||||
|
@ -61,6 +60,8 @@ EndProject
|
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control
|
||||
build\resources\deb\DEBIAN\preinst = build\resources\deb\DEBIAN\preinst
|
||||
build\resources\deb\DEBIAN\prerm = build\resources\deb\DEBIAN\prerm
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}"
|
||||
|
@ -81,7 +82,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C54D
|
|||
build\scripts\localization-check.js = build\scripts\localization-check.js
|
||||
build\scripts\package.linux.sh = build\scripts\package.linux.sh
|
||||
build\scripts\package.osx-app.sh = build\scripts\package.osx-app.sh
|
||||
build\scripts\package.windows-portable.sh = build\scripts\package.windows-portable.sh
|
||||
build\scripts\package.windows.sh = build\scripts\package.windows.sh
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
|
|
86
THIRD-PARTY-LICENSES.md
Normal file
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>
|
||||
<summary>Missing Keys</summary>
|
||||
<summary>Missing keys in de_DE.axaml</summary>
|
||||
|
||||
- Text.BranchCM.MergeMultiBranches
|
||||
- Text.CommitCM.Merge
|
||||
- Text.CommitCM.MergeMultiple
|
||||
- Text.CommitDetail.Files.Search
|
||||
- Text.Diff.UseBlockNavigation
|
||||
- Text.FileCM.ResolveUsing
|
||||
- Text.Hotkeys.Global.Clone
|
||||
- Text.InProgress.CherryPick.Head
|
||||
- Text.InProgress.Merge.Operating
|
||||
- Text.InProgress.Rebase.StoppedAt
|
||||
- Text.InProgress.Revert.Head
|
||||
- Text.Merge.Source
|
||||
- Text.MergeMultiple
|
||||
- Text.MergeMultiple.CommitChanges
|
||||
- Text.MergeMultiple.Strategy
|
||||
- Text.MergeMultiple.Targets
|
||||
- Text.Repository.Skip
|
||||
- Text.WorkingCopy.CommitToEdit
|
||||
- Text.BranchUpstreamInvalid
|
||||
- Text.Configure.CustomAction.WaitForExit
|
||||
- Text.Configure.IssueTracker.AddSampleAzure
|
||||
- Text.CopyFullPath
|
||||
- Text.Diff.First
|
||||
- Text.Diff.Last
|
||||
- Text.Preferences.AI.Streaming
|
||||
- Text.Preferences.Appearance.EditorTabWidth
|
||||
- Text.Preferences.General.ShowTagsInGraph
|
||||
- Text.StashCM.SaveAsPatch
|
||||
|
||||
</details>
|
||||
|
||||
### es_ES.axaml: 97.78%
|
||||
|
||||
### 
|
||||
|
||||
<details>
|
||||
<summary>Missing Keys</summary>
|
||||
<summary>Missing keys in es_ES.axaml</summary>
|
||||
|
||||
- Text.BranchCM.MergeMultiBranches
|
||||
- Text.CommitCM.Merge
|
||||
- Text.CommitCM.MergeMultiple
|
||||
- Text.Diff.UseBlockNavigation
|
||||
- Text.FileCM.ResolveUsing
|
||||
- Text.Hotkeys.Global.Clone
|
||||
- Text.InProgress.CherryPick.Head
|
||||
- Text.InProgress.Merge.Operating
|
||||
- Text.InProgress.Rebase.StoppedAt
|
||||
- Text.InProgress.Revert.Head
|
||||
- Text.Merge.Source
|
||||
- Text.MergeMultiple
|
||||
- Text.MergeMultiple.CommitChanges
|
||||
- Text.MergeMultiple.Strategy
|
||||
- Text.MergeMultiple.Targets
|
||||
- Text.Repository.Skip
|
||||
- Text.CopyFullPath
|
||||
|
||||
</details>
|
||||
|
||||
### fr_FR.axaml: 95.00%
|
||||
|
||||
### 
|
||||
|
||||
<details>
|
||||
<summary>Missing Keys</summary>
|
||||
<summary>Missing keys in fr_FR.axaml</summary>
|
||||
|
||||
- Text.BranchCM.MergeMultiBranches
|
||||
- Text.CherryPick.AppendSourceToMessage
|
||||
- Text.CherryPick.Mainline.Tips
|
||||
- Text.CommitCM.CherryPickMultiple
|
||||
- Text.CommitCM.Merge
|
||||
- Text.CommitCM.MergeMultiple
|
||||
- Text.CommitDetail.Files.Search
|
||||
- Text.Diff.UseBlockNavigation
|
||||
- Text.Fetch.Force
|
||||
- Text.FileCM.ResolveUsing
|
||||
- Text.Hotkeys.Global.Clone
|
||||
- Text.InProgress.CherryPick.Head
|
||||
- Text.InProgress.Merge.Operating
|
||||
- Text.InProgress.Rebase.StoppedAt
|
||||
- Text.InProgress.Revert.Head
|
||||
- Text.Merge.Source
|
||||
- Text.MergeMultiple
|
||||
- Text.MergeMultiple.CommitChanges
|
||||
- Text.MergeMultiple.Strategy
|
||||
- Text.MergeMultiple.Targets
|
||||
- Text.Preference.Appearance.FontSize
|
||||
- Text.Preference.Appearance.FontSize.Default
|
||||
- Text.Preference.Appearance.FontSize.Editor
|
||||
- Text.Preference.General.ShowChildren
|
||||
- Text.Repository.CustomActions
|
||||
- Text.Repository.FilterCommits
|
||||
- Text.Repository.FilterCommits.Default
|
||||
- Text.Repository.FilterCommits.Exclude
|
||||
- Text.Repository.FilterCommits.Include
|
||||
- Text.Repository.HistoriesOrder
|
||||
- Text.Repository.HistoriesOrder.ByDate
|
||||
- Text.Repository.HistoriesOrder.Topo
|
||||
- Text.Repository.Skip
|
||||
- Text.ScanRepositories
|
||||
- Text.SHALinkCM.NavigateTo
|
||||
- Text.WorkingCopy.CommitToEdit
|
||||
- Text.CopyFullPath
|
||||
|
||||
</details>
|
||||
|
||||
### it_IT.axaml: 95.56%
|
||||
|
||||
### 
|
||||
|
||||
<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.BranchUpstreamInvalid
|
||||
- Text.Clone.RecurseSubmodules
|
||||
- Text.CommitCM.Merge
|
||||
- Text.CommitCM.MergeMultiple
|
||||
- Text.CommitDetail.Files.Search
|
||||
- Text.CommitDetail.Info.Children
|
||||
- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
|
||||
- Text.Configure.OpenAI.Preferred
|
||||
- Text.Configure.OpenAI.Preferred.Tip
|
||||
- Text.Configure.CustomAction.Scope.Branch
|
||||
- Text.Configure.CustomAction.WaitForExit
|
||||
- Text.Configure.IssueTracker.AddSampleGiteeIssue
|
||||
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
|
||||
- Text.CopyFullPath
|
||||
- Text.CreateBranch.Name.WarnSpace
|
||||
- Text.DeleteRepositoryNode.Path
|
||||
- Text.DeleteRepositoryNode.TipForGroup
|
||||
- Text.DeleteRepositoryNode.TipForRepository
|
||||
- Text.Diff.First
|
||||
- Text.Diff.Last
|
||||
- Text.Diff.UseBlockNavigation
|
||||
- Text.Fetch.Force
|
||||
- Text.FileCM.ResolveUsing
|
||||
- Text.Hotkeys.Global.Clone
|
||||
- Text.InProgress.CherryPick.Head
|
||||
- Text.InProgress.Merge.Operating
|
||||
- Text.InProgress.Rebase.StoppedAt
|
||||
|
@ -121,93 +95,40 @@
|
|||
- Text.MergeMultiple.CommitChanges
|
||||
- Text.MergeMultiple.Strategy
|
||||
- Text.MergeMultiple.Targets
|
||||
- Text.Preference.General.ShowChildren
|
||||
- Text.Preferences.AI.Streaming
|
||||
- Text.Preferences.Appearance.EditorTabWidth
|
||||
- Text.Preferences.General.DateFormat
|
||||
- Text.Preferences.General.ShowChildren
|
||||
- Text.Preferences.General.ShowTagsInGraph
|
||||
- Text.Preferences.Git.SSLVerify
|
||||
- Text.Repository.FilterCommits
|
||||
- Text.Repository.FilterCommits.Default
|
||||
- Text.Repository.FilterCommits.Exclude
|
||||
- Text.Repository.FilterCommits.Include
|
||||
- Text.Repository.HistoriesLayout
|
||||
- Text.Repository.HistoriesLayout.Horizontal
|
||||
- Text.Repository.HistoriesLayout.Vertical
|
||||
- Text.Repository.HistoriesOrder
|
||||
- Text.Repository.HistoriesOrder.ByDate
|
||||
- Text.Repository.HistoriesOrder.Topo
|
||||
- Text.Repository.Notifications.Clear
|
||||
- Text.Repository.OnlyHighlightCurrentBranchInHistories
|
||||
- Text.Repository.Skip
|
||||
- Text.SHALinkCM.CopySHA
|
||||
- Text.Repository.Tags.OrderByCreatorDate
|
||||
- Text.Repository.Tags.OrderByNameAsc
|
||||
- Text.Repository.Tags.OrderByNameDes
|
||||
- Text.Repository.Tags.Sort
|
||||
- Text.Repository.UseRelativeTimeInHistories
|
||||
- Text.SetUpstream
|
||||
- Text.SetUpstream.Local
|
||||
- Text.SetUpstream.Unset
|
||||
- Text.SetUpstream.Upstream
|
||||
- Text.SHALinkCM.NavigateTo
|
||||
- Text.Stash.AutoRestore
|
||||
- Text.Stash.AutoRestore.Tip
|
||||
- Text.StashCM.SaveAsPatch
|
||||
- Text.WorkingCopy.CommitToEdit
|
||||
- Text.WorkingCopy.SignOff
|
||||
|
||||
</details>
|
||||
|
||||
### pt_BR.axaml: 96.81%
|
||||
### 
|
||||
|
||||
### 
|
||||
|
||||
<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
|
||||
Version: 8.23
|
||||
Version: 2025.10
|
||||
Priority: optional
|
||||
Depends: libx11-6, libice6, libsm6
|
||||
Depends: libx11-6, libice6, libsm6, libicu | libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu69 | libicu68 | libicu67 | libicu66 | libicu65 | libicu63 | libicu60 | libicu57 | libicu55 | libicu52, xdg-utils
|
||||
Architecture: amd64
|
||||
Installed-Size: 60440
|
||||
Maintainer: longshuang@msn.cn
|
||||
Description: Open-source & Free Git GUI Client
|
||||
|
|
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: libSM.so.6()(%{__isa_bits}bit)
|
||||
Requires: libicu
|
||||
Requires: xdg-utils
|
||||
|
||||
%define _build_id_links none
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ const repoRoot = path.join(__dirname, '../../');
|
|||
const localesDir = path.join(repoRoot, 'src/Resources/Locales');
|
||||
const enUSFile = path.join(localesDir, 'en_US.axaml');
|
||||
const outputFile = path.join(repoRoot, 'TRANSLATION.md');
|
||||
const readmeFile = path.join(repoRoot, 'README.md');
|
||||
|
||||
const parser = new xml2js.Parser();
|
||||
|
||||
|
@ -18,42 +17,36 @@ async function parseXml(filePath) {
|
|||
async function calculateTranslationRate() {
|
||||
const enUSData = await parseXml(enUSFile);
|
||||
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
|
||||
|
||||
const translationRates = [];
|
||||
const badges = [];
|
||||
|
||||
const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml'));
|
||||
|
||||
// Add en_US badge first
|
||||
badges.push(`[](TRANSLATION.md)`);
|
||||
const lines = [];
|
||||
|
||||
lines.push('# Translation Status');
|
||||
lines.push('This document shows the translation status of each locale file in the repository.');
|
||||
lines.push(`## Details`);
|
||||
lines.push(`### `);
|
||||
|
||||
for (const file of files) {
|
||||
const locale = file.replace('.axaml', '').replace('_', '__');
|
||||
const filePath = path.join(localesDir, file);
|
||||
const localeData = await parseXml(filePath);
|
||||
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
|
||||
|
||||
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
|
||||
const translationRate = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
|
||||
|
||||
translationRates.push(`### ${file}: ${translationRate.toFixed(2)}%\n`);
|
||||
translationRates.push(`<details>\n<summary>Missing Keys</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`);
|
||||
if (missingKeys.length > 0) {
|
||||
const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
|
||||
const badgeColor = progress >= 75 ? 'yellow' : 'red';
|
||||
|
||||
// Add badges
|
||||
const locale = file.replace('.axaml', '').replace('_', '__');
|
||||
const badgeColor = translationRate === 100 ? 'brightgreen' : translationRate >= 75 ? 'yellow' : 'red';
|
||||
badges.push(`[}%25-${badgeColor})](TRANSLATION.md)`);
|
||||
lines.push(`### }%25-${badgeColor})`);
|
||||
lines.push(`<details>\n<summary>Missing keys in ${file}</summary>\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n</details>`)
|
||||
} else {
|
||||
lines.push(`### `);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(translationRates.join('\n\n'));
|
||||
|
||||
await fs.writeFile(outputFile, translationRates.join('\n\n') + '\n', 'utf8');
|
||||
|
||||
// Update README.md
|
||||
let readmeContent = await fs.readFile(readmeFile, 'utf8');
|
||||
const badgeSection = `## Translation Status\n\n${badges.join(' ')}`;
|
||||
console.log(badgeSection);
|
||||
readmeContent = readmeContent.replace(/## Translation Status\n\n.*\n\n/, badgeSection + '\n\n');
|
||||
await fs.writeFile(readmeFile, readmeContent, 'utf8');
|
||||
const content = lines.join('\n\n');
|
||||
console.log(content);
|
||||
await fs.writeFile(outputFile, content, 'utf8');
|
||||
}
|
||||
|
||||
calculateTranslationRate().catch(err => console.error(err));
|
||||
|
|
|
@ -56,8 +56,15 @@ cp -f SourceGit/* resources/deb/opt/sourcegit
|
|||
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
|
||||
cp -r resources/_common/applications resources/deb/usr/share
|
||||
cp -r resources/_common/icons resources/deb/usr/share
|
||||
sed -i -e "s/^Version:.*/Version: $VERSION/" -e "s/^Architecture:.*/Architecture: $arch/" resources/deb/DEBIAN/control
|
||||
dpkg-deb --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
|
||||
# Calculate installed size in KB
|
||||
installed_size=$(du -sk resources/deb | cut -f1)
|
||||
# Update the control file
|
||||
sed -i -e "s/^Version:.*/Version: $VERSION/" \
|
||||
-e "s/^Architecture:.*/Architecture: $arch/" \
|
||||
-e "s/^Installed-Size:.*/Installed-Size: $installed_size/" \
|
||||
resources/deb/DEBIAN/control
|
||||
# Build deb package with gzip compression
|
||||
dpkg-deb -Zgzip --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
|
||||
|
||||
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
|
||||
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
public static readonly Command OpenPreferenceCommand = new Command(_ => OpenDialog(new Views.Preference()));
|
||||
public static bool IsCheckForUpdateCommandVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
#if DISABLE_UPDATE_DETECTION
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
|
||||
public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
|
||||
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
|
||||
public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
|
||||
public static readonly Command CheckForUpdateCommand = new Command(_ => Check4Update(true));
|
||||
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true));
|
||||
public static readonly Command QuitCommand = new Command(_ => Quit(0));
|
||||
public static readonly Command CopyTextBlockCommand = new Command(p => CopyTextBlock(p as TextBlock));
|
||||
public static readonly Command CopyTextBlockCommand = new Command(p =>
|
||||
{
|
||||
var textBlock = p as TextBlock;
|
||||
if (textBlock == null)
|
||||
return;
|
||||
|
||||
if (textBlock.Inlines is { Count: > 0 } inlines)
|
||||
CopyText(inlines.Text);
|
||||
else if (!string.IsNullOrEmpty(textBlock.Text))
|
||||
CopyText(textBlock.Text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,11 +46,9 @@ namespace SourceGit
|
|||
[JsonSerializable(typeof(Models.ExternalToolPaths))]
|
||||
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
|
||||
[JsonSerializable(typeof(Models.JetBrainsState))]
|
||||
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
|
||||
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
|
||||
[JsonSerializable(typeof(Models.ThemeOverrides))]
|
||||
[JsonSerializable(typeof(Models.Version))]
|
||||
[JsonSerializable(typeof(Models.RepositorySettings))]
|
||||
[JsonSerializable(typeof(ViewModels.Preference))]
|
||||
[JsonSerializable(typeof(ViewModels.Preferences))]
|
||||
internal partial class JsonCodeGen : JsonSerializerContext { }
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@
|
|||
<NativeMenu>
|
||||
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}"/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
|
||||
<NativeMenuItemSeparator/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.Preference}" Command="{x:Static s:App.OpenPreferenceCommand}" Gesture="⌘+,"/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
|
||||
<NativeMenuItemSeparator/>
|
||||
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>
|
||||
|
|
281
src/App.axaml.cs
281
src/App.axaml.cs
|
@ -1,10 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia;
|
||||
|
@ -22,6 +24,7 @@ namespace SourceGit
|
|||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
#region App Entry Point
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
@ -34,15 +37,14 @@ namespace SourceGit
|
|||
|
||||
TaskScheduler.UnobservedTaskException += (_, e) =>
|
||||
{
|
||||
LogException(e.Exception);
|
||||
e.SetObserved();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo))
|
||||
if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo))
|
||||
Environment.Exit(exitTodo);
|
||||
else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage))
|
||||
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
|
||||
Environment.Exit(exitMessage);
|
||||
else
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
|
@ -75,34 +77,33 @@ namespace SourceGit
|
|||
return builder;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
private static void LogException(Exception ex)
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
if (ex == null)
|
||||
return;
|
||||
|
||||
var pref = ViewModels.Preference.Instance;
|
||||
pref.PropertyChanged += (_, _) => pref.Save();
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
|
||||
builder.Append("----------------------------\n");
|
||||
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
|
||||
builder.Append($"OS: {Environment.OSVersion}\n");
|
||||
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
|
||||
builder.Append($"Source: {ex.Source}\n");
|
||||
builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
|
||||
builder.Append($"User: {Environment.UserName}\n");
|
||||
builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
|
||||
builder.Append($"Exception Time: {DateTime.Now}\n");
|
||||
builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
|
||||
builder.Append($"---------------------------\n\n");
|
||||
builder.Append(ex);
|
||||
|
||||
SetLocale(pref.Locale);
|
||||
SetTheme(pref.Theme, pref.ThemeOverrides);
|
||||
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
BindingPlugins.DataValidators.RemoveAt(0);
|
||||
|
||||
if (TryLaunchedAsCoreEditor(desktop))
|
||||
return;
|
||||
|
||||
if (TryLaunchedAsAskpass(desktop))
|
||||
return;
|
||||
|
||||
TryLaunchedAsNormal(desktop);
|
||||
}
|
||||
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
|
||||
File.WriteAllText(file, builder.ToString());
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Utility Functions
|
||||
public static void OpenDialog(Window window)
|
||||
{
|
||||
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||
|
@ -204,6 +205,9 @@ namespace SourceGit
|
|||
app._fontsOverrides = null;
|
||||
}
|
||||
|
||||
defaultFont = app.FixFontFamilyName(defaultFont);
|
||||
monospaceFont = app.FixFontFamilyName(monospaceFont);
|
||||
|
||||
var resDic = new ResourceDictionary();
|
||||
if (!string.IsNullOrEmpty(defaultFont))
|
||||
resDic.Add("Fonts.Default", new FontFamily(defaultFont));
|
||||
|
@ -304,21 +308,6 @@ namespace SourceGit
|
|||
return Current is App app ? app._launcher : null;
|
||||
}
|
||||
|
||||
public static ViewModels.Repository FindOpenedRepository(string repoPath)
|
||||
{
|
||||
if (Current is App app && app._launcher != null)
|
||||
{
|
||||
foreach (var page in app._launcher.Pages)
|
||||
{
|
||||
var id = page.Node.Id.Replace("\\", "/");
|
||||
if (id == repoPath && page.Data is ViewModels.Repository repo)
|
||||
return repo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void Quit(int exitCode)
|
||||
{
|
||||
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
|
@ -331,94 +320,39 @@ namespace SourceGit
|
|||
Environment.Exit(exitCode);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void CopyTextBlock(TextBlock textBlock)
|
||||
#region Overrides
|
||||
public override void Initialize()
|
||||
{
|
||||
if (textBlock == null)
|
||||
return;
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
if (textBlock.Inlines is { Count: > 0 } inlines)
|
||||
CopyText(inlines.Text);
|
||||
else if (!string.IsNullOrEmpty(textBlock.Text))
|
||||
CopyText(textBlock.Text);
|
||||
var pref = ViewModels.Preferences.Instance;
|
||||
pref.PropertyChanged += (_, _) => pref.Save();
|
||||
|
||||
SetLocale(pref.Locale);
|
||||
SetTheme(pref.Theme, pref.ThemeOverrides);
|
||||
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
|
||||
}
|
||||
|
||||
private static void LogException(Exception ex)
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ex == null)
|
||||
return;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
|
||||
builder.Append("----------------------------\n");
|
||||
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
|
||||
builder.Append($"OS: {Environment.OSVersion.ToString()}\n");
|
||||
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
|
||||
builder.Append($"Source: {ex.Source}\n");
|
||||
builder.Append($"---------------------------\n\n");
|
||||
builder.Append(ex.StackTrace);
|
||||
while (ex.InnerException != null)
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
ex = ex.InnerException;
|
||||
builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n");
|
||||
builder.Append(ex.StackTrace);
|
||||
BindingPlugins.DataValidators.RemoveAt(0);
|
||||
|
||||
if (TryLaunchAsCoreEditor(desktop))
|
||||
return;
|
||||
|
||||
if (TryLaunchAsAskpass(desktop))
|
||||
return;
|
||||
|
||||
TryLaunchAsNormal(desktop);
|
||||
}
|
||||
|
||||
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
|
||||
File.WriteAllText(file, builder.ToString());
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void Check4Update(bool manually = false)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fetch lastest release information.
|
||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) };
|
||||
var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json");
|
||||
|
||||
// Parse json into Models.Version.
|
||||
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
|
||||
if (ver == null)
|
||||
return;
|
||||
|
||||
// Check if already up-to-date.
|
||||
if (!ver.IsNewVersion)
|
||||
{
|
||||
if (manually)
|
||||
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
|
||||
return;
|
||||
}
|
||||
|
||||
// Should not check ignored tag if this is called manually.
|
||||
if (!manually)
|
||||
{
|
||||
var pref = ViewModels.Preference.Instance;
|
||||
if (ver.TagName == pref.IgnoreUpdateTag)
|
||||
return;
|
||||
}
|
||||
|
||||
ShowSelfUpdateResult(ver);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (manually)
|
||||
ShowSelfUpdateResult(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void ShowSelfUpdateResult(object data)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
OpenDialog(new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } });
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode)
|
||||
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = -1;
|
||||
|
||||
|
@ -471,7 +405,7 @@ namespace SourceGit
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode)
|
||||
private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = -1;
|
||||
|
||||
|
@ -505,7 +439,7 @@ namespace SourceGit
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var args = desktop.Args;
|
||||
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
|
||||
|
@ -513,14 +447,18 @@ namespace SourceGit
|
|||
|
||||
var file = args[1];
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
desktop.Shutdown(-1);
|
||||
else
|
||||
desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
var editor = new Views.StandaloneCommitMessageEditor();
|
||||
editor.SetFile(file);
|
||||
desktop.MainWindow = editor;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS");
|
||||
if (launchAsAskpass is not "TRUE")
|
||||
|
@ -529,14 +467,16 @@ namespace SourceGit
|
|||
var args = desktop.Args;
|
||||
if (args?.Length > 0)
|
||||
{
|
||||
desktop.MainWindow = new Views.Askpass(args[0]);
|
||||
var askpass = new Views.Askpass();
|
||||
askpass.TxtDescription.Text = args[0];
|
||||
desktop.MainWindow = askpass;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
Native.OS.SetupEnternalTools();
|
||||
Models.AvatarManager.Instance.Start();
|
||||
|
@ -548,9 +488,96 @@ namespace SourceGit
|
|||
_launcher = new ViewModels.Launcher(startupRepo);
|
||||
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
|
||||
|
||||
var pref = ViewModels.Preference.Instance;
|
||||
#if !DISABLE_UPDATE_DETECTION
|
||||
var pref = ViewModels.Preferences.Instance;
|
||||
if (pref.ShouldCheck4UpdateOnStartup())
|
||||
Check4Update();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Check4Update(bool manually = false)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fetch latest release information.
|
||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) };
|
||||
var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json");
|
||||
|
||||
// Parse JSON into Models.Version.
|
||||
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
|
||||
if (ver == null)
|
||||
return;
|
||||
|
||||
// Check if already up-to-date.
|
||||
if (!ver.IsNewVersion)
|
||||
{
|
||||
if (manually)
|
||||
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
|
||||
return;
|
||||
}
|
||||
|
||||
// Should not check ignored tag if this is called manually.
|
||||
if (!manually)
|
||||
{
|
||||
var pref = ViewModels.Preferences.Instance;
|
||||
if (ver.TagName == pref.IgnoreUpdateTag)
|
||||
return;
|
||||
}
|
||||
|
||||
ShowSelfUpdateResult(ver);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (manually)
|
||||
ShowSelfUpdateResult(new Models.SelfUpdateFailed(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ShowSelfUpdateResult(object data)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||
{
|
||||
var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } };
|
||||
dialog.ShowDialog(owner);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private string FixFontFamilyName(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return string.Empty;
|
||||
|
||||
var parts = input.Split(',');
|
||||
var trimmed = new List<string>();
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var t = part.Trim();
|
||||
if (string.IsNullOrEmpty(t))
|
||||
continue;
|
||||
|
||||
// Collapse multiple spaces into single space
|
||||
var prevChar = '\0';
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var c in t)
|
||||
{
|
||||
if (c == ' ' && prevChar == ' ')
|
||||
continue;
|
||||
sb.Append(c);
|
||||
prevChar = c;
|
||||
}
|
||||
|
||||
trimmed.Add(sb.ToString());
|
||||
}
|
||||
|
||||
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
|
||||
}
|
||||
|
||||
private ViewModels.Launcher _launcher = null;
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace SourceGit.Commands
|
|||
Args = includeUntracked ? "add ." : "add -u .";
|
||||
}
|
||||
|
||||
public Add(string repo, List<Models.Change> changes)
|
||||
public Add(string repo, List<string> changes)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
@ -22,10 +22,17 @@ namespace SourceGit.Commands
|
|||
foreach (var c in changes)
|
||||
{
|
||||
builder.Append(" \"");
|
||||
builder.Append(c.Path);
|
||||
builder.Append(c);
|
||||
builder.Append("\"");
|
||||
}
|
||||
Args = builder.ToString();
|
||||
}
|
||||
|
||||
public Add(string repo, string pathspecFromFile)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"add --pathspec-from-file=\"{pathspecFromFile}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ namespace SourceGit.Commands
|
|||
var commit = match.Groups[1].Value;
|
||||
var author = match.Groups[2].Value;
|
||||
var timestamp = int.Parse(match.Groups[3].Value);
|
||||
var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd");
|
||||
var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString(_dateFormat);
|
||||
|
||||
var info = new Models.BlameLineInfo()
|
||||
{
|
||||
|
@ -87,6 +87,7 @@ namespace SourceGit.Commands
|
|||
|
||||
private readonly Models.BlameData _result = new Models.BlameData();
|
||||
private readonly StringBuilder _content = new StringBuilder();
|
||||
private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
|
||||
private string _lastSHA = string.Empty;
|
||||
private bool _needUnifyCommitSHA = false;
|
||||
private int _minSHALen = 64;
|
||||
|
|
|
@ -54,21 +54,14 @@
|
|||
|
||||
public static bool DeleteRemote(string repo, string remote, string name)
|
||||
{
|
||||
bool exists = new Remote(repo).HasBranch(remote, name);
|
||||
if (exists)
|
||||
return new Push(repo, remote, $"refs/heads/{name}", true).Exec();
|
||||
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
|
||||
bool exists = new Remote(repo).HasBranch(remote, name);
|
||||
if (exists)
|
||||
{
|
||||
cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
cmd.Args = $"push {remote} --delete {name}";
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd.Args = $"branch -D -r {remote}/{name}";
|
||||
}
|
||||
|
||||
cmd.Args = $"branch -D -r {remote}/{name}";
|
||||
return cmd.Exec();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace SourceGit.Commands
|
|||
WorkingDirectory = path;
|
||||
TraitErrorAsOutput = true;
|
||||
SSHKey = sshKey;
|
||||
Args = "clone --progress --verbose --recurse-submodules ";
|
||||
Args = "clone --progress --verbose ";
|
||||
|
||||
if (!string.IsNullOrEmpty(extraArgs))
|
||||
Args += $"{extraArgs} ";
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
|
@ -10,11 +11,6 @@ namespace SourceGit.Commands
|
|||
{
|
||||
public partial class Command
|
||||
{
|
||||
public class CancelToken
|
||||
{
|
||||
public bool Requested { get; set; } = false;
|
||||
}
|
||||
|
||||
public class ReadToEndResult
|
||||
{
|
||||
public bool IsSuccess { get; set; } = false;
|
||||
|
@ -30,7 +26,7 @@ namespace SourceGit.Commands
|
|||
}
|
||||
|
||||
public string Context { get; set; } = string.Empty;
|
||||
public CancelToken Cancel { get; set; } = null;
|
||||
public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
|
||||
public string WorkingDirectory { get; set; } = null;
|
||||
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
|
||||
public string SSHKey { get; set; } = string.Empty;
|
||||
|
@ -43,36 +39,15 @@ namespace SourceGit.Commands
|
|||
var start = CreateGitStartInfo();
|
||||
var errs = new List<string>();
|
||||
var proc = new Process() { StartInfo = start };
|
||||
var isCancelled = false;
|
||||
|
||||
proc.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (Cancel != null && Cancel.Requested)
|
||||
{
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
if (!proc.HasExited)
|
||||
proc.Kill(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Data != null)
|
||||
OnReadline(e.Data);
|
||||
};
|
||||
|
||||
proc.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (Cancel != null && Cancel.Requested)
|
||||
{
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
if (!proc.HasExited)
|
||||
proc.Kill(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(e.Data))
|
||||
{
|
||||
errs.Add(string.Empty);
|
||||
|
@ -97,9 +72,25 @@ namespace SourceGit.Commands
|
|||
errs.Add(e.Data);
|
||||
};
|
||||
|
||||
var dummy = null as Process;
|
||||
var dummyProcLock = new object();
|
||||
try
|
||||
{
|
||||
proc.Start();
|
||||
|
||||
// It not safe, please only use `CancellationToken` in readonly commands.
|
||||
if (CancellationToken.CanBeCanceled)
|
||||
{
|
||||
dummy = proc;
|
||||
CancellationToken.Register(() =>
|
||||
{
|
||||
lock (dummyProcLock)
|
||||
{
|
||||
if (dummy is { HasExited: false })
|
||||
dummy.Kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -113,15 +104,23 @@ namespace SourceGit.Commands
|
|||
proc.BeginErrorReadLine();
|
||||
proc.WaitForExit();
|
||||
|
||||
if (dummy != null)
|
||||
{
|
||||
lock (dummyProcLock)
|
||||
{
|
||||
dummy = null;
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = proc.ExitCode;
|
||||
proc.Close();
|
||||
|
||||
if (!isCancelled && exitCode != 0)
|
||||
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
|
||||
{
|
||||
if (RaiseError)
|
||||
{
|
||||
var errMsg = string.Join("\n", errs);
|
||||
if (!string.IsNullOrWhiteSpace(errMsg))
|
||||
var errMsg = string.Join("\n", errs).Trim();
|
||||
if (!string.IsNullOrEmpty(errMsg))
|
||||
Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg));
|
||||
}
|
||||
|
||||
|
@ -192,13 +191,12 @@ namespace SourceGit.Commands
|
|||
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
|
||||
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
|
||||
|
||||
// Force using en_US.UTF-8 locale to avoid GCM crash
|
||||
// Force using en_US.UTF-8 locale
|
||||
if (OperatingSystem.IsLinux())
|
||||
start.Environment.Add("LANG", "en_US.UTF-8");
|
||||
|
||||
// Fix macOS `PATH` env
|
||||
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
|
||||
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
|
||||
{
|
||||
start.Environment.Add("LANG", "C");
|
||||
start.Environment.Add("LC_ALL", "C");
|
||||
}
|
||||
|
||||
// Force using this app as git editor.
|
||||
switch (Editor)
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
public partial class CompareRevisions : Command
|
||||
{
|
||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
||||
[GeneratedRegex(@"^([MADRC])\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
public CompareRevisions(string repo, string start, string end)
|
||||
|
@ -18,6 +18,15 @@ namespace SourceGit.Commands
|
|||
Args = $"diff --name-status {based} {end}";
|
||||
}
|
||||
|
||||
public CompareRevisions(string repo, string start, string end, string path)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
var based = string.IsNullOrEmpty(start) ? "-R" : start;
|
||||
Args = $"diff --name-status {based} {end} -- \"{path}\"";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result()
|
||||
{
|
||||
Exec();
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace SourceGit.Commands
|
|||
var rs = new Dictionary<string, string>();
|
||||
if (output.IsSuccess)
|
||||
{
|
||||
var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var idx = line.IndexOf('=', StringComparison.Ordinal);
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "status -uno --ignore-submodules=dirty --porcelain";
|
||||
Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain";
|
||||
}
|
||||
|
||||
public int Result()
|
||||
|
@ -16,7 +16,7 @@ namespace SourceGit.Commands
|
|||
var rs = ReadToEnd();
|
||||
if (rs.IsSuccess)
|
||||
{
|
||||
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
return lines.Length;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,18 @@ namespace SourceGit.Commands
|
|||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("deleted file mode ", StringComparison.Ordinal))
|
||||
{
|
||||
_result.OldMode = line.Substring(18);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("new file mode ", StringComparison.Ordinal))
|
||||
{
|
||||
_result.NewMode = line.Substring(14);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_result.IsBinary)
|
||||
return;
|
||||
|
||||
|
|
|
@ -8,7 +8,26 @@ namespace SourceGit.Commands
|
|||
{
|
||||
public static class ExecuteCustomAction
|
||||
{
|
||||
public static void Run(string repo, string file, string args, Action<string> outputHandler)
|
||||
public static void Run(string repo, string file, string args)
|
||||
{
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = file;
|
||||
start.Arguments = args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.WorkingDirectory = repo;
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(start);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
|
||||
{
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = file;
|
||||
|
@ -21,14 +40,6 @@ namespace SourceGit.Commands
|
|||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
start.WorkingDirectory = repo;
|
||||
|
||||
// Force using en_US.UTF-8 locale to avoid GCM crash
|
||||
if (OperatingSystem.IsLinux())
|
||||
start.Environment.Add("LANG", "en_US.UTF-8");
|
||||
|
||||
// Fix macOS `PATH` env
|
||||
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
|
||||
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
|
||||
|
||||
var proc = new Process() { StartInfo = start };
|
||||
var builder = new StringBuilder();
|
||||
|
||||
|
@ -53,26 +64,21 @@ namespace SourceGit.Commands
|
|||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
proc.WaitForExit();
|
||||
|
||||
var exitCode = proc.ExitCode;
|
||||
if (exitCode != 0)
|
||||
{
|
||||
var errMsg = builder.ToString().Trim();
|
||||
if (!string.IsNullOrEmpty(errMsg))
|
||||
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, errMsg));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(repo, e.Message);
|
||||
});
|
||||
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
|
||||
}
|
||||
|
||||
var exitCode = proc.ExitCode;
|
||||
proc.Close();
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
var errMsg = builder.ToString();
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(repo, errMsg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
public class Fetch : Command
|
||||
{
|
||||
public Fetch(string repo, string remote, bool noTags, bool prune, bool force, Action<string> outputHandler)
|
||||
public Fetch(string repo, string remote, bool noTags, bool force, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
|
@ -21,9 +21,6 @@ namespace SourceGit.Commands
|
|||
if (force)
|
||||
Args += "--force ";
|
||||
|
||||
if (prune)
|
||||
Args += "--prune ";
|
||||
|
||||
Args += remote;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -20,82 +22,78 @@ namespace SourceGit.Commands
|
|||
}
|
||||
}
|
||||
|
||||
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
|
||||
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
|
||||
{
|
||||
_service = service;
|
||||
_repo = repo;
|
||||
_changes = changes;
|
||||
_cancelToken = cancelToken;
|
||||
_onProgress = onProgress;
|
||||
_onResponse = onResponse;
|
||||
}
|
||||
|
||||
public string Result()
|
||||
public void Exec()
|
||||
{
|
||||
try
|
||||
{
|
||||
var summarybuilder = new StringBuilder();
|
||||
var bodyBuilder = new StringBuilder();
|
||||
_onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
|
||||
|
||||
var responseBuilder = new StringBuilder();
|
||||
var summaryBuilder = new StringBuilder();
|
||||
foreach (var change in _changes)
|
||||
{
|
||||
if (_cancelToken.IsCancellationRequested)
|
||||
return "";
|
||||
return;
|
||||
|
||||
_onProgress?.Invoke($"Analyzing {change.Path}...");
|
||||
responseBuilder.Append("- ");
|
||||
summaryBuilder.Append("- ");
|
||||
|
||||
var summary = GenerateChangeSummary(change);
|
||||
summarybuilder.Append("- ");
|
||||
summarybuilder.Append(summary);
|
||||
summarybuilder.Append("(file: ");
|
||||
summarybuilder.Append(change.Path);
|
||||
summarybuilder.Append(")");
|
||||
summarybuilder.AppendLine();
|
||||
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
|
||||
if (rs.IsSuccess)
|
||||
{
|
||||
_service.Chat(
|
||||
_service.AnalyzeDiffPrompt,
|
||||
$"Here is the `git diff` output: {rs.StdOut}",
|
||||
_cancelToken,
|
||||
update =>
|
||||
{
|
||||
responseBuilder.Append(update);
|
||||
summaryBuilder.Append(update);
|
||||
|
||||
bodyBuilder.Append("- ");
|
||||
bodyBuilder.Append(summary);
|
||||
bodyBuilder.AppendLine();
|
||||
_onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}");
|
||||
});
|
||||
}
|
||||
|
||||
responseBuilder.Append("\n");
|
||||
summaryBuilder.Append("(file: ");
|
||||
summaryBuilder.Append(change.Path);
|
||||
summaryBuilder.Append(")\n");
|
||||
}
|
||||
|
||||
if (_cancelToken.IsCancellationRequested)
|
||||
return "";
|
||||
return;
|
||||
|
||||
_onProgress?.Invoke($"Generating commit message...");
|
||||
|
||||
var body = bodyBuilder.ToString();
|
||||
var subject = GenerateSubject(summarybuilder.ToString());
|
||||
return string.Format("{0}\n\n{1}", subject, body);
|
||||
var responseBody = responseBuilder.ToString();
|
||||
var subjectBuilder = new StringBuilder();
|
||||
_service.Chat(
|
||||
_service.GenerateSubjectPrompt,
|
||||
$"Here are the summaries changes:\n{summaryBuilder}",
|
||||
_cancelToken,
|
||||
update =>
|
||||
{
|
||||
subjectBuilder.Append(update);
|
||||
_onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
|
||||
return "";
|
||||
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateChangeSummary(Models.Change change)
|
||||
{
|
||||
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
|
||||
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
|
||||
|
||||
var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
|
||||
if (rsp != null && rsp.Choices.Count > 0)
|
||||
return rsp.Choices[0].Message.Content;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private string GenerateSubject(string summary)
|
||||
{
|
||||
var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
|
||||
if (rsp != null && rsp.Choices.Count > 0)
|
||||
return rsp.Choices[0].Message.Content;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private Models.OpenAIService _service;
|
||||
private string _repo;
|
||||
private List<Models.Change> _changes;
|
||||
private CancellationToken _cancelToken;
|
||||
private Action<string> _onProgress;
|
||||
private Action<string> _onResponse;
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
{
|
||||
[GeneratedRegex(@"^(.+)\s+(\w+)\s+\w+:(\d+)$")]
|
||||
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
|
||||
private static partial Regex REG_LOCK();
|
||||
|
||||
class SubCmd : Command
|
||||
|
@ -82,7 +82,7 @@ namespace SourceGit.Commands
|
|||
var rs = cmd.ReadToEnd();
|
||||
if (rs.IsSuccess)
|
||||
{
|
||||
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var match = REG_LOCK().Match(line);
|
||||
|
|
|
@ -4,21 +4,20 @@ namespace SourceGit.Commands
|
|||
{
|
||||
public class Pull : Command
|
||||
{
|
||||
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, bool prune, Action<string> outputHandler)
|
||||
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
Args = "pull --verbose --progress --tags ";
|
||||
Args = "pull --verbose --progress ";
|
||||
|
||||
if (useRebase)
|
||||
Args += "--rebase ";
|
||||
Args += "--rebase=true ";
|
||||
|
||||
if (noTags)
|
||||
Args += "--no-tags ";
|
||||
if (prune)
|
||||
Args += "--prune ";
|
||||
|
||||
Args += $"{remote} {branch}";
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace SourceGit.Commands
|
|||
Args += $"{remote} {local}:{remoteBranch}";
|
||||
}
|
||||
|
||||
public Push(string repo, string remote, string tag, bool isDelete)
|
||||
public Push(string repo, string remote, string refname, bool isDelete)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
@ -36,7 +36,7 @@ namespace SourceGit.Commands
|
|||
if (isDelete)
|
||||
Args += "--delete ";
|
||||
|
||||
Args += $"{remote} refs/tags/{tag}";
|
||||
Args += $"{remote} {refname}";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
|
|
|
@ -24,12 +24,23 @@ namespace SourceGit.Commands
|
|||
if (!rs.IsSuccess)
|
||||
return branches;
|
||||
|
||||
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
var remoteBranches = new HashSet<string>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var b = ParseLine(line);
|
||||
if (b != null)
|
||||
{
|
||||
branches.Add(b);
|
||||
if (!b.IsLocal)
|
||||
remoteBranches.Add(b.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var b in branches)
|
||||
{
|
||||
if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream))
|
||||
b.IsUpsteamGone = !remoteBranches.Contains(b.Upstream);
|
||||
}
|
||||
|
||||
return branches;
|
||||
|
@ -75,6 +86,7 @@ namespace SourceGit.Commands
|
|||
branch.Head = parts[1];
|
||||
branch.IsCurrent = parts[2] == "*";
|
||||
branch.Upstream = parts[3];
|
||||
branch.IsUpsteamGone = false;
|
||||
|
||||
if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
|
||||
branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();
|
||||
|
|
|
@ -9,10 +9,10 @@ namespace SourceGit.Commands
|
|||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
_commit = commit;
|
||||
Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
|
||||
Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}";
|
||||
}
|
||||
|
||||
public IEnumerable<string> Result()
|
||||
public List<string> Result()
|
||||
{
|
||||
Exec();
|
||||
return _lines;
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"show --no-show-signature --pretty=format:%B -s {sha}";
|
||||
Args = $"show --no-show-signature --format=%B -s {sha}";
|
||||
}
|
||||
|
||||
public string Result()
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
const string baseArgs = "show --no-show-signature --pretty=format:\"%G?%n%GS%n%GK\" -s";
|
||||
const string baseArgs = "show --no-show-signature --format=%G?%n%GS%n%GK -s";
|
||||
const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null";
|
||||
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
|||
if (!rs.IsSuccess)
|
||||
return null;
|
||||
|
||||
var raw = rs.StdOut.Trim();
|
||||
var raw = rs.StdOut.Trim().ReplaceLineEndings("\n");
|
||||
if (raw.Length <= 1)
|
||||
return null;
|
||||
|
||||
|
@ -29,7 +29,6 @@
|
|||
Signer = lines[1],
|
||||
Key = lines[2]
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,11 @@ namespace SourceGit.Commands
|
|||
{
|
||||
public class QueryCommits : Command
|
||||
{
|
||||
public QueryCommits(string repo, bool useTopoOrder, string limits, bool needFindHead = true)
|
||||
public QueryCommits(string repo, string limits, bool needFindHead = true)
|
||||
{
|
||||
var order = useTopoOrder ? "--topo-order" : "--date-order";
|
||||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"log {order} --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
|
||||
Args = $"log --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
|
||||
_findFirstMerged = needFindHead;
|
||||
}
|
||||
|
||||
|
@ -20,9 +18,13 @@ namespace SourceGit.Commands
|
|||
{
|
||||
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
|
||||
|
||||
if (method == Models.CommitSearchMethod.ByUser)
|
||||
if (method == Models.CommitSearchMethod.ByAuthor)
|
||||
{
|
||||
search += $"-i --author=\"{filter}\" --committer=\"{filter}\"";
|
||||
search += $"-i --author=\"{filter}\"";
|
||||
}
|
||||
else if (method == Models.CommitSearchMethod.ByCommitter)
|
||||
{
|
||||
search += $"-i --committer=\"{filter}\"";
|
||||
}
|
||||
else if (method == Models.CommitSearchMethod.ByFile)
|
||||
{
|
||||
|
@ -33,7 +35,7 @@ namespace SourceGit.Commands
|
|||
var argsBuilder = new StringBuilder();
|
||||
argsBuilder.Append(search);
|
||||
|
||||
var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var word in words)
|
||||
{
|
||||
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||
|
@ -46,7 +48,7 @@ namespace SourceGit.Commands
|
|||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
|
||||
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
|
||||
_findFirstMerged = false;
|
||||
}
|
||||
|
||||
|
@ -122,7 +124,7 @@ namespace SourceGit.Commands
|
|||
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
|
||||
|
||||
var rs = ReadToEnd();
|
||||
var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (shas.Length == 0)
|
||||
return;
|
||||
|
||||
|
|
|
@ -3,18 +3,18 @@ using System.Collections.Generic;
|
|||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class QueryCommitsWithFullMessage : Command
|
||||
public class QueryCommitsForInteractiveRebase : Command
|
||||
{
|
||||
public QueryCommitsWithFullMessage(string repo, string args)
|
||||
public QueryCommitsForInteractiveRebase(string repo, string on)
|
||||
{
|
||||
_boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----";
|
||||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {args}";
|
||||
Args = $"log --date-order --no-show-signature --decorate=full --format=\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {on}..HEAD";
|
||||
}
|
||||
|
||||
public List<Models.CommitWithMessage> Result()
|
||||
public List<Models.InteractiveCommit> Result()
|
||||
{
|
||||
var rs = ReadToEnd();
|
||||
if (!rs.IsSuccess)
|
||||
|
@ -29,7 +29,7 @@ namespace SourceGit.Commands
|
|||
switch (nextPartIdx)
|
||||
{
|
||||
case 0:
|
||||
_current = new Models.CommitWithMessage();
|
||||
_current = new Models.InteractiveCommit();
|
||||
_current.Commit.SHA = line;
|
||||
_commits.Add(_current);
|
||||
break;
|
||||
|
@ -52,7 +52,7 @@ namespace SourceGit.Commands
|
|||
_current.Commit.CommitterTime = ulong.Parse(line);
|
||||
break;
|
||||
default:
|
||||
var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
|
||||
var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal);
|
||||
if (boundary > end)
|
||||
{
|
||||
_current.Message = rs.StdOut.Substring(start, boundary - start - 1);
|
||||
|
@ -88,8 +88,8 @@ namespace SourceGit.Commands
|
|||
_current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
private List<Models.CommitWithMessage> _commits = new List<Models.CommitWithMessage>();
|
||||
private Models.CommitWithMessage _current = null;
|
||||
private List<Models.InteractiveCommit> _commits = [];
|
||||
private Models.InteractiveCommit _current = null;
|
||||
private string _boundary = "";
|
||||
}
|
||||
}
|
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;
|
||||
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()
|
||||
|
|
|
@ -20,9 +20,12 @@ namespace SourceGit.Commands
|
|||
if (!output.IsSuccess)
|
||||
return rs;
|
||||
|
||||
var lines = output.StdOut.Split('\n');
|
||||
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.EndsWith("/HEAD", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (line.StartsWith("refs/heads/", StringComparison.Ordinal))
|
||||
rs.Add(new() { Name = line.Substring("refs/heads/".Length), Type = Models.DecoratorType.LocalBranchHead });
|
||||
else if (line.StartsWith("refs/remotes/", StringComparison.Ordinal))
|
||||
|
|
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
|
||||
{
|
||||
|
@ -9,13 +11,17 @@
|
|||
Args = $"ls-tree -r -z --name-only {revision}";
|
||||
}
|
||||
|
||||
public string[] Result()
|
||||
public List<string> Result()
|
||||
{
|
||||
var rs = ReadToEnd();
|
||||
if (rs.IsSuccess)
|
||||
return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
|
||||
if (!rs.IsSuccess)
|
||||
return [];
|
||||
|
||||
return [];
|
||||
var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
|
||||
var outs = new List<string>();
|
||||
foreach (var line in lines)
|
||||
outs.Add(line);
|
||||
return outs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
|
||||
Args = $"show --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
|
||||
}
|
||||
|
||||
public Models.Commit Result()
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace SourceGit.Commands
|
|||
if (rs.IsSuccess)
|
||||
{
|
||||
var changes = new List<Models.Change>();
|
||||
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var match = REG_FORMAT2().Match(line);
|
||||
|
|
|
@ -1,60 +1,68 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Query stash changes. Requires git >= 2.32.0
|
||||
/// </summary>
|
||||
public partial class QueryStashChanges : Command
|
||||
{
|
||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
||||
[GeneratedRegex(@"^([MADRC])\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
public QueryStashChanges(string repo, string sha)
|
||||
public QueryStashChanges(string repo, string stash)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
|
||||
Args = $"stash show -u --name-status \"{stash}\"";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result()
|
||||
{
|
||||
Exec();
|
||||
return _changes;
|
||||
}
|
||||
var rs = ReadToEnd();
|
||||
if (!rs.IsSuccess)
|
||||
return [];
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
var match = REG_FORMAT().Match(line);
|
||||
if (!match.Success)
|
||||
return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0])
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
var outs = new List<Models.Change>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
case 'M':
|
||||
change.Set(Models.ChangeState.Modified);
|
||||
_changes.Add(change);
|
||||
break;
|
||||
case 'A':
|
||||
change.Set(Models.ChangeState.Added);
|
||||
_changes.Add(change);
|
||||
break;
|
||||
case 'D':
|
||||
change.Set(Models.ChangeState.Deleted);
|
||||
_changes.Add(change);
|
||||
break;
|
||||
case 'R':
|
||||
change.Set(Models.ChangeState.Renamed);
|
||||
_changes.Add(change);
|
||||
break;
|
||||
case 'C':
|
||||
change.Set(Models.ChangeState.Copied);
|
||||
_changes.Add(change);
|
||||
break;
|
||||
}
|
||||
}
|
||||
var match = REG_FORMAT().Match(line);
|
||||
if (!match.Success)
|
||||
continue;
|
||||
|
||||
private readonly List<Models.Change> _changes = new List<Models.Change>();
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0])
|
||||
{
|
||||
case 'M':
|
||||
change.Set(Models.ChangeState.Modified);
|
||||
outs.Add(change);
|
||||
break;
|
||||
case 'A':
|
||||
change.Set(Models.ChangeState.Added);
|
||||
outs.Add(change);
|
||||
break;
|
||||
case 'D':
|
||||
change.Set(Models.ChangeState.Deleted);
|
||||
outs.Add(change);
|
||||
break;
|
||||
case 'R':
|
||||
change.Set(Models.ChangeState.Renamed);
|
||||
outs.Add(change);
|
||||
break;
|
||||
case 'C':
|
||||
change.Set(Models.ChangeState.Copied);
|
||||
outs.Add(change);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
outs.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
|
||||
return outs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
|
@ -8,7 +9,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s";
|
||||
Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s";
|
||||
}
|
||||
|
||||
public List<Models.Stash> Result()
|
||||
|
@ -26,21 +27,32 @@ namespace SourceGit.Commands
|
|||
_stashes.Add(_current);
|
||||
break;
|
||||
case 1:
|
||||
_current.Time = ulong.Parse(line);
|
||||
ParseParent(line);
|
||||
break;
|
||||
case 2:
|
||||
_current.Name = line;
|
||||
_current.Time = ulong.Parse(line);
|
||||
break;
|
||||
case 3:
|
||||
_current.Name = line;
|
||||
break;
|
||||
case 4:
|
||||
_current.Message = line;
|
||||
break;
|
||||
}
|
||||
|
||||
_nextLineIdx++;
|
||||
if (_nextLineIdx > 3)
|
||||
if (_nextLineIdx > 4)
|
||||
_nextLineIdx = 0;
|
||||
}
|
||||
|
||||
private void ParseParent(string data)
|
||||
{
|
||||
if (data.Length < 8)
|
||||
return;
|
||||
|
||||
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
|
||||
private Models.Stash _current = null;
|
||||
private int _nextLineIdx = 0;
|
||||
|
|
|
@ -24,11 +24,9 @@ namespace SourceGit.Commands
|
|||
{
|
||||
var submodules = new List<Models.Submodule>();
|
||||
var rs = ReadToEnd();
|
||||
if (!rs.IsSuccess)
|
||||
return submodules;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var match = REG_FORMAT1().Match(line);
|
||||
|
@ -51,13 +49,13 @@ namespace SourceGit.Commands
|
|||
|
||||
if (submodules.Count > 0)
|
||||
{
|
||||
Args = $"status -uno --porcelain -- {builder}";
|
||||
Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
|
||||
rs = ReadToEnd();
|
||||
if (!rs.IsSuccess)
|
||||
return submodules;
|
||||
|
||||
var dirty = new HashSet<string>();
|
||||
lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
|
||||
lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var match = REG_FORMAT_STATUS().Match(line);
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace SourceGit.Commands
|
|||
|
||||
Context = repo;
|
||||
WorkingDirectory = repo;
|
||||
Args = $"tag -l --sort=-creatordate --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(contents:subject)%0a%0a%(contents:body)\"";
|
||||
Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\"";
|
||||
}
|
||||
|
||||
public List<Models.Tag> Result()
|
||||
|
@ -25,14 +25,15 @@ namespace SourceGit.Commands
|
|||
foreach (var record in records)
|
||||
{
|
||||
var subs = record.Split('\0', StringSplitOptions.None);
|
||||
if (subs.Length != 4)
|
||||
if (subs.Length != 5)
|
||||
continue;
|
||||
|
||||
var message = subs[3].Trim();
|
||||
var message = subs[4].Trim();
|
||||
tags.Add(new Models.Tag()
|
||||
{
|
||||
Name = subs[0].Substring(10),
|
||||
SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
|
||||
CreatorDate = ulong.Parse(subs[3]),
|
||||
Message = string.IsNullOrEmpty(message) ? null : message,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace SourceGit.Commands
|
|||
if (!rs.IsSuccess)
|
||||
return status;
|
||||
|
||||
var lines = rs.StdOut.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line[0] == '>')
|
||||
|
|
|
@ -37,6 +37,19 @@ namespace SourceGit.Commands
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool ProcessStashChanges(string repo, List<Models.DiffOption> opts, string saveTo)
|
||||
{
|
||||
using (var sw = File.Create(saveTo))
|
||||
{
|
||||
foreach (var opt in opts)
|
||||
{
|
||||
if (!ProcessSingleChange(repo, opt, sw))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
|
||||
{
|
||||
var starter = new ProcessStartInfo();
|
||||
|
|
|
@ -11,72 +11,84 @@ namespace SourceGit.Commands
|
|||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Push(string message)
|
||||
{
|
||||
Args = $"stash push -m \"{message}\"";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Push(List<Models.Change> changes, string message, bool onlyStaged, bool keepIndex)
|
||||
public bool Push(string message, bool includeUntracked = true, bool keepIndex = false)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("stash push ");
|
||||
if (onlyStaged)
|
||||
builder.Append("--staged ");
|
||||
if (includeUntracked)
|
||||
builder.Append("--include-untracked ");
|
||||
if (keepIndex)
|
||||
builder.Append("--keep-index ");
|
||||
builder.Append("-m \"");
|
||||
builder.Append(message);
|
||||
builder.Append("\"");
|
||||
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Push(string message, List<Models.Change> changes, bool keepIndex)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("stash push --include-untracked ");
|
||||
if (keepIndex)
|
||||
builder.Append("--keep-index ");
|
||||
builder.Append("-m \"");
|
||||
builder.Append(message);
|
||||
builder.Append("\" -- ");
|
||||
|
||||
if (onlyStaged)
|
||||
{
|
||||
foreach (var c in changes)
|
||||
builder.Append($"\"{c.Path}\" ");
|
||||
}
|
||||
else
|
||||
{
|
||||
var needAdd = new List<Models.Change>();
|
||||
foreach (var c in changes)
|
||||
{
|
||||
builder.Append($"\"{c.Path}\" ");
|
||||
|
||||
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked)
|
||||
{
|
||||
needAdd.Add(c);
|
||||
if (needAdd.Count > 10)
|
||||
{
|
||||
new Add(WorkingDirectory, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needAdd.Count > 0)
|
||||
{
|
||||
new Add(WorkingDirectory, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
}
|
||||
foreach (var c in changes)
|
||||
builder.Append($"\"{c.Path}\" ");
|
||||
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Apply(string name)
|
||||
public bool Push(string message, string pathspecFromFile, bool keepIndex)
|
||||
{
|
||||
Args = $"stash apply -q {name}";
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("stash push --include-untracked --pathspec-from-file=\"");
|
||||
builder.Append(pathspecFromFile);
|
||||
builder.Append("\" ");
|
||||
if (keepIndex)
|
||||
builder.Append("--keep-index ");
|
||||
builder.Append("-m \"");
|
||||
builder.Append(message);
|
||||
builder.Append("\"");
|
||||
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool PushOnlyStaged(string message, bool keepIndex)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("stash push --staged ");
|
||||
if (keepIndex)
|
||||
builder.Append("--keep-index ");
|
||||
builder.Append("-m \"");
|
||||
builder.Append(message);
|
||||
builder.Append("\"");
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Apply(string name, bool restoreIndex)
|
||||
{
|
||||
var opts = restoreIndex ? "--index" : string.Empty;
|
||||
Args = $"stash apply -q {opts} \"{name}\"";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Pop(string name)
|
||||
{
|
||||
Args = $"stash pop -q {name}";
|
||||
Args = $"stash pop -q --index \"{name}\"";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Drop(string name)
|
||||
{
|
||||
Args = $"stash drop -q {name}";
|
||||
Args = $"stash drop -q \"{name}\"";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace SourceGit.Commands
|
|||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
|
||||
Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN";
|
||||
}
|
||||
|
||||
public Models.Statistics Result()
|
||||
|
|
|
@ -48,9 +48,7 @@ namespace SourceGit.Commands
|
|||
if (remotes != null)
|
||||
{
|
||||
foreach (var r in remotes)
|
||||
{
|
||||
new Push(repo, r.Name, name, true).Exec();
|
||||
}
|
||||
new Push(repo, r.Name, $"refs/tags/{name}", true).Exec();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -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.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
|
@ -20,12 +21,13 @@ namespace SourceGit.Commands
|
|||
var last = null as Models.Worktree;
|
||||
if (rs.IsSuccess)
|
||||
{
|
||||
var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith("worktree ", StringComparison.Ordinal))
|
||||
{
|
||||
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
|
||||
last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath);
|
||||
worktrees.Add(last);
|
||||
}
|
||||
else if (line.StartsWith("bare", StringComparison.Ordinal))
|
||||
|
@ -73,6 +75,8 @@ namespace SourceGit.Commands
|
|||
|
||||
if (!string.IsNullOrEmpty(tracking))
|
||||
Args += tracking;
|
||||
else if (!string.IsNullOrEmpty(name) && !createNew)
|
||||
Args += name;
|
||||
|
||||
_outputHandler = outputHandler;
|
||||
return Exec();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Converters
|
||||
{
|
||||
|
@ -6,5 +7,8 @@ namespace SourceGit.Converters
|
|||
{
|
||||
public static readonly FuncValueConverter<bool, double> ToPageTabWidth =
|
||||
new FuncValueConverter<bool, double>(x => x ? 200 : double.NaN);
|
||||
|
||||
public static readonly FuncValueConverter<bool, FontWeight> IsBoldToFontWeight =
|
||||
new FuncValueConverter<bool, FontWeight>(x => x ? FontWeight.Bold : FontWeight.Normal);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ namespace SourceGit.Converters
|
|||
new FuncValueConverter<int, bool>(v => v != 1);
|
||||
|
||||
public static readonly FuncValueConverter<int, bool> IsSubjectLengthBad =
|
||||
new FuncValueConverter<int, bool>(v => v > ViewModels.Preference.Instance.SubjectGuideLength);
|
||||
new FuncValueConverter<int, bool>(v => v > ViewModels.Preferences.Instance.SubjectGuideLength);
|
||||
|
||||
public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood =
|
||||
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength);
|
||||
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preferences.Instance.SubjectGuideLength);
|
||||
|
||||
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
|
||||
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace SourceGit.Converters
|
||||
{
|
||||
public static partial class StringConverters
|
||||
public static class StringConverters
|
||||
{
|
||||
public class ToLocaleConverter : IValueConverter
|
||||
{
|
||||
|
@ -68,22 +67,6 @@ namespace SourceGit.Converters
|
|||
public static readonly FuncValueConverter<string, string> ToShortSHA =
|
||||
new FuncValueConverter<string, string>(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v));
|
||||
|
||||
public static readonly FuncValueConverter<string, bool> UnderRecommendGitVersion =
|
||||
new(v =>
|
||||
{
|
||||
var match = REG_GIT_VERSION().Match(v ?? "");
|
||||
if (match.Success)
|
||||
{
|
||||
var major = int.Parse(match.Groups[1].Value);
|
||||
var minor = int.Parse(match.Groups[2].Value);
|
||||
var build = int.Parse(match.Groups[3].Value);
|
||||
|
||||
return new Version(major, minor, build) < MINIMAL_GIT_VERSION;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
public static readonly FuncValueConverter<string, string> TrimRefsPrefix =
|
||||
new FuncValueConverter<string, string>(v =>
|
||||
{
|
||||
|
@ -96,9 +79,7 @@ namespace SourceGit.Converters
|
|||
return v;
|
||||
});
|
||||
|
||||
[GeneratedRegex(@"^[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")]
|
||||
private static partial Regex REG_GIT_VERSION();
|
||||
|
||||
private static readonly Version MINIMAL_GIT_VERSION = new Version(2, 23, 0);
|
||||
public static readonly FuncValueConverter<string, bool> ContainsSpaces =
|
||||
new FuncValueConverter<string, bool>(v => v != null && v.Contains(' '));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,22 @@
|
|||
{
|
||||
public class ApplyWhiteSpaceMode
|
||||
{
|
||||
public static readonly ApplyWhiteSpaceMode[] Supported =
|
||||
[
|
||||
new ApplyWhiteSpaceMode("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
|
||||
new ApplyWhiteSpaceMode("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
|
||||
new ApplyWhiteSpaceMode("Error", "Raise errors and refuses to apply the patch", "error"),
|
||||
new ApplyWhiteSpaceMode("Error All", "Similar to 'error', but shows more", "error-all"),
|
||||
];
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
|
||||
public ApplyWhiteSpaceMode(string n, string d, string a)
|
||||
{
|
||||
Name = App.Text(n);
|
||||
Desc = App.Text(d);
|
||||
Name = n;
|
||||
Desc = d;
|
||||
Arg = a;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -35,7 +35,7 @@ namespace SourceGit.Models
|
|||
|
||||
private static AvatarManager _instance = null;
|
||||
|
||||
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")]
|
||||
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")]
|
||||
private static partial Regex REG_GITHUB_USER_EMAIL();
|
||||
|
||||
private object _synclock = new object();
|
||||
|
@ -43,6 +43,7 @@ namespace SourceGit.Models
|
|||
private List<IAvatarHost> _avatars = new List<IAvatarHost>();
|
||||
private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
|
||||
private HashSet<string> _requesting = new HashSet<string>();
|
||||
private HashSet<string> _defaultAvatars = new HashSet<string>();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
|
@ -50,8 +51,8 @@ namespace SourceGit.Models
|
|||
if (!Directory.Exists(_storePath))
|
||||
Directory.CreateDirectory(_storePath);
|
||||
|
||||
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute));
|
||||
_resources.Add("noreply@github.com", new Bitmap(icon));
|
||||
LoadDefaultAvatar("noreply@github.com", "github.png");
|
||||
LoadDefaultAvatar("unrealbot@epicgames.com", "unreal.png");
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
|
@ -140,7 +141,7 @@ namespace SourceGit.Models
|
|||
{
|
||||
if (forceRefetch)
|
||||
{
|
||||
if (email.Equals("noreply@github.com", StringComparison.Ordinal))
|
||||
if (_defaultAvatars.Contains(email))
|
||||
return null;
|
||||
|
||||
if (_resources.ContainsKey(email))
|
||||
|
@ -185,11 +186,18 @@ namespace SourceGit.Models
|
|||
return null;
|
||||
}
|
||||
|
||||
private void LoadDefaultAvatar(string key, string img)
|
||||
{
|
||||
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute));
|
||||
_resources.Add(key, new Bitmap(icon));
|
||||
_defaultAvatars.Add(key);
|
||||
}
|
||||
|
||||
private string GetEmailHash(string email)
|
||||
{
|
||||
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
|
||||
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
|
||||
var builder = new StringBuilder();
|
||||
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered).AsSpan());
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
foreach (var c in hash)
|
||||
builder.Append(c.ToString("x2"));
|
||||
return builder.ToString();
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace SourceGit.Models
|
|||
public string Upstream { get; set; }
|
||||
public BranchTrackStatus TrackStatus { get; set; }
|
||||
public string Remote { get; set; }
|
||||
public bool IsUpsteamGone { get; set; }
|
||||
|
||||
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ namespace SourceGit.Models
|
|||
{
|
||||
public enum CommitSearchMethod
|
||||
{
|
||||
ByUser,
|
||||
BySHA = 0,
|
||||
ByAuthor,
|
||||
ByCommitter,
|
||||
ByMessage,
|
||||
ByFile,
|
||||
}
|
||||
|
@ -31,9 +33,10 @@ namespace SourceGit.Models
|
|||
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
|
||||
public bool HasDecorators => Decorators.Count > 0;
|
||||
|
||||
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd");
|
||||
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
|
||||
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
|
||||
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
|
||||
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
|
||||
|
||||
public bool IsMerged { get; set; } = false;
|
||||
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
|
||||
|
@ -111,9 +114,9 @@ namespace SourceGit.Models
|
|||
}
|
||||
}
|
||||
|
||||
public class CommitWithMessage
|
||||
public class CommitFullMessage
|
||||
{
|
||||
public Commit Commit { get; set; } = new Commit();
|
||||
public string Message { get; set; } = "";
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public List<Hyperlink> Links { get; set; } = [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,11 @@ namespace SourceGit.Models
|
|||
s_penCount = colors.Count;
|
||||
}
|
||||
|
||||
public class Path(int color)
|
||||
public class Path(int color, bool isMerged)
|
||||
{
|
||||
public List<Point> Points { get; } = [];
|
||||
public int Color { get; } = color;
|
||||
public bool IsMerged { get; } = isMerged;
|
||||
}
|
||||
|
||||
public class Link
|
||||
|
@ -37,6 +38,7 @@ namespace SourceGit.Models
|
|||
public Point Control;
|
||||
public Point End;
|
||||
public int Color;
|
||||
public bool IsMerged;
|
||||
}
|
||||
|
||||
public enum DotType
|
||||
|
@ -51,6 +53,7 @@ namespace SourceGit.Models
|
|||
public DotType Type;
|
||||
public Point Center;
|
||||
public int Color;
|
||||
public bool IsMerged;
|
||||
}
|
||||
|
||||
public List<Path> Paths { get; } = [];
|
||||
|
@ -68,7 +71,7 @@ namespace SourceGit.Models
|
|||
var unsolved = new List<PathHelper>();
|
||||
var ended = new List<PathHelper>();
|
||||
var offsetY = -halfHeight;
|
||||
var colorIdx = 0;
|
||||
var colorPicker = new ColorPicker();
|
||||
|
||||
foreach (var commit in commits)
|
||||
{
|
||||
|
@ -108,7 +111,6 @@ namespace SourceGit.Models
|
|||
}
|
||||
|
||||
isMerged = isMerged || l.IsMerged;
|
||||
major.IsMerged = isMerged;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -119,28 +121,35 @@ namespace SourceGit.Models
|
|||
|
||||
// Remove ended curves from unsolved
|
||||
foreach (var l in ended)
|
||||
{
|
||||
colorPicker.Recycle(l.Path.Color);
|
||||
unsolved.Remove(l);
|
||||
}
|
||||
ended.Clear();
|
||||
|
||||
// Create new curve for branch head
|
||||
// If no path found, create new curve for branch head
|
||||
// Otherwise, create new curve for new merged commit
|
||||
if (major == null)
|
||||
{
|
||||
offsetX += unitWidth;
|
||||
|
||||
if (commit.Parents.Count > 0)
|
||||
{
|
||||
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
|
||||
major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY));
|
||||
unsolved.Add(major);
|
||||
temp.Paths.Add(major.Path);
|
||||
}
|
||||
|
||||
colorIdx = (colorIdx + 1) % s_penCount;
|
||||
}
|
||||
else if (isMerged && !major.IsMerged && commit.Parents.Count > 0)
|
||||
{
|
||||
major.ReplaceMerged();
|
||||
temp.Paths.Add(major.Path);
|
||||
}
|
||||
|
||||
// Calculate link position of this commit.
|
||||
var position = new Point(major?.LastX ?? offsetX, offsetY);
|
||||
var dotColor = major?.Path.Color ?? 0;
|
||||
var anchor = new Dot() { Center = position, Color = dotColor };
|
||||
var anchor = new Dot() { Center = position, Color = dotColor, IsMerged = isMerged };
|
||||
if (commit.IsCurrentHead)
|
||||
anchor.Type = DotType.Head;
|
||||
else if (commit.Parents.Count > 1)
|
||||
|
@ -158,16 +167,20 @@ namespace SourceGit.Models
|
|||
var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal));
|
||||
if (parent != null)
|
||||
{
|
||||
// Try to change the merge state of linked graph
|
||||
if (isMerged)
|
||||
parent.IsMerged = true;
|
||||
if (isMerged && !parent.IsMerged)
|
||||
{
|
||||
parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight);
|
||||
parent.ReplaceMerged();
|
||||
temp.Paths.Add(parent.Path);
|
||||
}
|
||||
|
||||
temp.Links.Add(new Link
|
||||
{
|
||||
Start = position,
|
||||
End = new Point(parent.LastX, offsetY + halfHeight),
|
||||
Control = new Point(parent.LastX, position.Y),
|
||||
Color = parent.Path.Color
|
||||
Color = parent.Path.Color,
|
||||
IsMerged = isMerged,
|
||||
});
|
||||
}
|
||||
else
|
||||
|
@ -175,10 +188,9 @@ namespace SourceGit.Models
|
|||
offsetX += unitWidth;
|
||||
|
||||
// Create new curve for parent commit that not includes before
|
||||
var l = new PathHelper(parentHash, isMerged, colorIdx, position, new Point(offsetX, position.Y + halfHeight));
|
||||
var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight));
|
||||
unsolved.Add(l);
|
||||
temp.Paths.Add(l.Path);
|
||||
colorIdx = (colorIdx + 1) % s_penCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,32 +217,53 @@ namespace SourceGit.Models
|
|||
return temp;
|
||||
}
|
||||
|
||||
private class ColorPicker
|
||||
{
|
||||
public int Next()
|
||||
{
|
||||
if (_colorsQueue.Count == 0)
|
||||
{
|
||||
for (var i = 0; i < s_penCount; i++)
|
||||
_colorsQueue.Enqueue(i);
|
||||
}
|
||||
|
||||
return _colorsQueue.Dequeue();
|
||||
}
|
||||
|
||||
public void Recycle(int idx)
|
||||
{
|
||||
if (!_colorsQueue.Contains(idx))
|
||||
_colorsQueue.Enqueue(idx);
|
||||
}
|
||||
|
||||
private Queue<int> _colorsQueue = new Queue<int>();
|
||||
}
|
||||
|
||||
private class PathHelper
|
||||
{
|
||||
public Path Path { get; }
|
||||
public Path Path { get; private set; }
|
||||
public string Next { get; set; }
|
||||
public bool IsMerged { get; set; }
|
||||
public double LastX { get; private set; }
|
||||
|
||||
public bool IsMerged => Path.IsMerged;
|
||||
|
||||
public PathHelper(string next, bool isMerged, int color, Point start)
|
||||
{
|
||||
Next = next;
|
||||
IsMerged = isMerged;
|
||||
LastX = start.X;
|
||||
_lastY = start.Y;
|
||||
|
||||
Path = new Path(color);
|
||||
Path = new Path(color, isMerged);
|
||||
Path.Points.Add(start);
|
||||
}
|
||||
|
||||
public PathHelper(string next, bool isMerged, int color, Point start, Point to)
|
||||
{
|
||||
Next = next;
|
||||
IsMerged = isMerged;
|
||||
LastX = to.X;
|
||||
_lastY = to.Y;
|
||||
|
||||
Path = new Path(color);
|
||||
Path = new Path(color, isMerged);
|
||||
Path.Points.Add(start);
|
||||
Path.Points.Add(to);
|
||||
}
|
||||
|
@ -310,6 +343,19 @@ namespace SourceGit.Models
|
|||
_lastY = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End the current path and create a new from the end.
|
||||
/// </summary>
|
||||
public void ReplaceMerged()
|
||||
{
|
||||
var color = Path.Color;
|
||||
Add(LastX, _lastY);
|
||||
|
||||
Path = new Path(color, true);
|
||||
Path.Points.Add(new Point(LastX, _lastY));
|
||||
_endY = 0;
|
||||
}
|
||||
|
||||
private void Add(double x, double y)
|
||||
{
|
||||
if (_endY < y)
|
||||
|
@ -327,7 +373,6 @@ namespace SourceGit.Models
|
|||
private static readonly List<Color> s_defaultPenColors = [
|
||||
Colors.Orange,
|
||||
Colors.ForestGreen,
|
||||
Colors.Gray,
|
||||
Colors.Turquoise,
|
||||
Colors.Olive,
|
||||
Colors.Magenta,
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace SourceGit.Models
|
|||
{
|
||||
Repository,
|
||||
Commit,
|
||||
Branch,
|
||||
}
|
||||
|
||||
public class CustomAction : ObservableObject
|
||||
|
@ -34,9 +35,16 @@ namespace SourceGit.Models
|
|||
set => SetProperty(ref _arguments, value);
|
||||
}
|
||||
|
||||
public bool WaitForExit
|
||||
{
|
||||
get => _waitForExit;
|
||||
set => SetProperty(ref _waitForExit, value);
|
||||
}
|
||||
|
||||
private string _name = string.Empty;
|
||||
private CustomActionScope _scope = CustomActionScope.Repository;
|
||||
private string _executable = string.Empty;
|
||||
private string _arguments = string.Empty;
|
||||
private bool _waitForExit = true;
|
||||
}
|
||||
}
|
||||
|
|
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 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(5, "kdiff3", "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMerger(6, "beyond_compare", "Beyond Compare", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e -sw \"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""),
|
||||
};
|
||||
|
|
30
src/Models/GitVersions.cs
Normal file
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
|
||||
{
|
||||
string FullPath { get; set; }
|
||||
string GitDir { get; set; }
|
||||
|
||||
void RefreshBranches();
|
||||
void RefreshWorktrees();
|
||||
void RefreshTags();
|
||||
|
|
|
@ -12,6 +12,12 @@ namespace SourceGit.Models
|
|||
Drop,
|
||||
}
|
||||
|
||||
public class InteractiveCommit
|
||||
{
|
||||
public Commit Commit { get; set; } = new Commit();
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class InteractiveRebaseJob
|
||||
{
|
||||
public string SHA { get; set; } = string.Empty;
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
public static readonly MergeMode[] Supported =
|
||||
[
|
||||
new MergeMode("Default", "Fast-forward if possible", ""),
|
||||
new MergeMode("Fast-forward", "Refuse to merge when fast-forward is not possible", "--ff-only"),
|
||||
new MergeMode("No Fast-forward", "Always create a merge commit", "--no-ff"),
|
||||
new MergeMode("Squash", "Use '--squash'", "--squash"),
|
||||
new MergeMode("Squash", "Squash merge", "--squash"),
|
||||
new MergeMode("Don't commit", "Merge without commit", "--no-ff --no-commit"),
|
||||
];
|
||||
|
||||
|
|
|
@ -1,79 +1,99 @@
|
|||
using System;
|
||||
using System.ClientModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class OpenAIChatMessage
|
||||
public partial class OpenAIResponse
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role
|
||||
public OpenAIResponse(Action<string> onUpdate)
|
||||
{
|
||||
get;
|
||||
set;
|
||||
_onUpdate = onUpdate;
|
||||
}
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content
|
||||
public void Append(string text)
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
var buffer = text;
|
||||
|
||||
public class OpenAIChatChoice
|
||||
{
|
||||
[JsonPropertyName("index")]
|
||||
public int Index
|
||||
{
|
||||
get;
|
||||
set;
|
||||
if (_thinkTail.Length > 0)
|
||||
{
|
||||
_thinkTail.Append(buffer);
|
||||
buffer = _thinkTail.ToString();
|
||||
_thinkTail.Clear();
|
||||
}
|
||||
|
||||
buffer = REG_COT().Replace(buffer, "");
|
||||
|
||||
var startIdx = buffer.IndexOf('<', StringComparison.Ordinal);
|
||||
if (startIdx >= 0)
|
||||
{
|
||||
if (startIdx > 0)
|
||||
OnReceive(buffer.Substring(0, startIdx));
|
||||
|
||||
var endIdx = buffer.IndexOf(">", startIdx + 1, StringComparison.Ordinal);
|
||||
if (endIdx <= startIdx)
|
||||
{
|
||||
if (buffer.Length - startIdx <= 15)
|
||||
_thinkTail.Append(buffer.Substring(startIdx));
|
||||
else
|
||||
OnReceive(buffer.Substring(startIdx));
|
||||
}
|
||||
else if (endIdx < startIdx + 15)
|
||||
{
|
||||
var tag = buffer.Substring(startIdx + 1, endIdx - startIdx - 1);
|
||||
if (_thinkTags.Contains(tag))
|
||||
_thinkTail.Append(buffer.Substring(startIdx));
|
||||
else
|
||||
OnReceive(buffer.Substring(startIdx));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnReceive(buffer.Substring(startIdx));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OnReceive(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public OpenAIChatMessage Message
|
||||
public void End()
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenAIChatResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public List<OpenAIChatChoice> Choices
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = [];
|
||||
}
|
||||
|
||||
public class OpenAIChatRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model
|
||||
{
|
||||
get;
|
||||
set;
|
||||
if (_thinkTail.Length > 0)
|
||||
{
|
||||
OnReceive(_thinkTail.ToString());
|
||||
_thinkTail.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<OpenAIChatMessage> Messages
|
||||
private void OnReceive(string text)
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = [];
|
||||
if (!_hasTrimmedStart)
|
||||
{
|
||||
text = text.TrimStart();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
public void AddMessage(string role, string content)
|
||||
{
|
||||
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
|
||||
_hasTrimmedStart = true;
|
||||
}
|
||||
|
||||
_onUpdate.Invoke(text);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"<(think|thought|thinking|thought_chain)>.*?</\1>", RegexOptions.Singleline)]
|
||||
private static partial Regex REG_COT();
|
||||
|
||||
private Action<string> _onUpdate = null;
|
||||
private StringBuilder _thinkTail = new StringBuilder();
|
||||
private HashSet<string> _thinkTags = ["think", "thought", "thinking", "thought_chain"];
|
||||
private bool _hasTrimmedStart = false;
|
||||
}
|
||||
|
||||
public class OpenAIService : ObservableObject
|
||||
|
@ -87,7 +107,15 @@ namespace SourceGit.Models
|
|||
public string Server
|
||||
{
|
||||
get => _server;
|
||||
set => SetProperty(ref _server, value);
|
||||
set
|
||||
{
|
||||
// migrate old server value
|
||||
if (!string.IsNullOrEmpty(value) && value.EndsWith("/chat/completions", StringComparison.Ordinal))
|
||||
{
|
||||
value = value.Substring(0, value.Length - "/chat/completions".Length);
|
||||
}
|
||||
SetProperty(ref _server, value);
|
||||
}
|
||||
}
|
||||
|
||||
public string ApiKey
|
||||
|
@ -102,6 +130,12 @@ namespace SourceGit.Models
|
|||
set => SetProperty(ref _model, value);
|
||||
}
|
||||
|
||||
public bool Streaming
|
||||
{
|
||||
get => _streaming;
|
||||
set => SetProperty(ref _streaming, value);
|
||||
}
|
||||
|
||||
public string AnalyzeDiffPrompt
|
||||
{
|
||||
get => _analyzeDiffPrompt;
|
||||
|
@ -147,45 +181,54 @@ namespace SourceGit.Models
|
|||
""";
|
||||
}
|
||||
|
||||
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
|
||||
public void Chat(string prompt, string question, CancellationToken cancellation, Action<string> onUpdate)
|
||||
{
|
||||
var chat = new OpenAIChatRequest() { Model = Model };
|
||||
chat.AddMessage("user", prompt);
|
||||
chat.AddMessage("user", question);
|
||||
|
||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
|
||||
if (!string.IsNullOrEmpty(ApiKey))
|
||||
var server = new Uri(_server);
|
||||
var key = new ApiKeyCredential(_apiKey);
|
||||
var client = null as ChatClient;
|
||||
if (_server.Contains("openai.azure.com/", StringComparison.Ordinal))
|
||||
{
|
||||
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
|
||||
client.DefaultRequestHeaders.Add("api-key", ApiKey);
|
||||
else
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
||||
var azure = new AzureOpenAIClient(server, key);
|
||||
client = azure.GetChatClient(_model);
|
||||
}
|
||||
else
|
||||
{
|
||||
var openai = new OpenAIClient(key, new() { Endpoint = server });
|
||||
client = openai.GetChatClient(_model);
|
||||
}
|
||||
|
||||
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
|
||||
var messages = new List<ChatMessage>();
|
||||
messages.Add(_model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt));
|
||||
messages.Add(new UserChatMessage(question));
|
||||
|
||||
try
|
||||
{
|
||||
var task = client.PostAsync(Server, req, cancellation);
|
||||
task.Wait(cancellation);
|
||||
var rsp = new OpenAIResponse(onUpdate);
|
||||
|
||||
var rsp = task.Result;
|
||||
var reader = rsp.Content.ReadAsStringAsync(cancellation);
|
||||
reader.Wait(cancellation);
|
||||
|
||||
var body = reader.Result;
|
||||
if (!rsp.IsSuccessStatusCode)
|
||||
if (_streaming)
|
||||
{
|
||||
throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}");
|
||||
var updates = client.CompleteChatStreaming(messages, null, cancellation);
|
||||
|
||||
foreach (var update in updates)
|
||||
{
|
||||
if (update.ContentUpdate.Count > 0)
|
||||
rsp.Append(update.ContentUpdate[0].Text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var completion = client.CompleteChat(messages, null, cancellation);
|
||||
|
||||
if (completion.Value.Content.Count > 0)
|
||||
rsp.Append(completion.Value.Content[0].Text);
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
|
||||
rsp.End();
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (cancellation.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
throw;
|
||||
if (!cancellation.IsCancellationRequested)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,6 +236,7 @@ namespace SourceGit.Models
|
|||
private string _server;
|
||||
private string _apiKey;
|
||||
private string _model;
|
||||
private bool _streaming = true;
|
||||
private string _analyzeDiffPrompt;
|
||||
private string _generateSubjectPrompt;
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ namespace SourceGit.Models
|
|||
{
|
||||
[GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")]
|
||||
private static partial Regex REG_HTTPS();
|
||||
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.git)?$")]
|
||||
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
|
||||
private static partial Regex REG_SSH1();
|
||||
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~]+/[\w\-\.]+(\.git)?$")]
|
||||
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")]
|
||||
private static partial Regex REG_SSH2();
|
||||
|
||||
[GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~]+/[\w\-\.]+)\.git$")]
|
||||
[GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~%]+/[\w\-\.%]+)\.git$")]
|
||||
private static partial Regex REG_TO_VISIT_URL_CAPTURE();
|
||||
|
||||
private static readonly Regex[] URL_FORMATS = [
|
||||
|
|
|
@ -14,13 +14,43 @@ namespace SourceGit.Models
|
|||
set;
|
||||
} = string.Empty;
|
||||
|
||||
public DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch
|
||||
public bool EnableReflog
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = DealWithLocalChanges.DoNothing;
|
||||
} = false;
|
||||
|
||||
public bool EnablePruneOnFetch
|
||||
public bool EnableFirstParentInHistories
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public bool EnableTopoOrderInHistories
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public bool OnlyHighlighCurrentBranchInHistories
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public TagSortMode TagSortMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TagSortMode.CreatorDate;
|
||||
|
||||
public bool IncludeUntrackedInLocalChanges
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
public bool EnableForceOnFetch
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -32,12 +62,6 @@ namespace SourceGit.Models
|
|||
set;
|
||||
} = false;
|
||||
|
||||
public DealWithLocalChanges DealWithLocalChangesOnPull
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = DealWithLocalChanges.DoNothing;
|
||||
|
||||
public bool PreferRebaseInsteadOfMerge
|
||||
{
|
||||
get;
|
||||
|
@ -68,11 +92,17 @@ namespace SourceGit.Models
|
|||
set;
|
||||
} = false;
|
||||
|
||||
public DealWithLocalChanges DealWithLocalChangesOnCreateBranch
|
||||
public bool PushToRemoteWhenCreateTag
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = DealWithLocalChanges.DoNothing;
|
||||
} = true;
|
||||
|
||||
public bool PushToRemoteWhenDeleteTag
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public bool CheckoutBranchOnCreateBranch
|
||||
{
|
||||
|
@ -84,31 +114,31 @@ namespace SourceGit.Models
|
|||
{
|
||||
get;
|
||||
set;
|
||||
} = new AvaloniaList<Filter>();
|
||||
} = [];
|
||||
|
||||
public AvaloniaList<CommitTemplate> CommitTemplates
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new AvaloniaList<CommitTemplate>();
|
||||
} = [];
|
||||
|
||||
public AvaloniaList<string> CommitMessages
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new AvaloniaList<string>();
|
||||
} = [];
|
||||
|
||||
public AvaloniaList<IssueTrackerRule> IssueTrackerRules
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new AvaloniaList<IssueTrackerRule>();
|
||||
} = [];
|
||||
|
||||
public AvaloniaList<CustomAction> CustomActions
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new AvaloniaList<CustomAction>();
|
||||
} = [];
|
||||
|
||||
public bool EnableAutoFetch
|
||||
{
|
||||
|
@ -146,12 +176,54 @@ namespace SourceGit.Models
|
|||
set;
|
||||
} = false;
|
||||
|
||||
public bool AutoRestoreAfterStash
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public string PreferedOpenAIService
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = "---";
|
||||
|
||||
public bool IsLocalBranchesExpandedInSideBar
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
public bool IsRemotesExpandedInSideBar
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public bool IsTagsExpandedInSideBar
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public bool IsSubmodulesExpandedInSideBar
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public bool IsWorktreeExpandedInSideBar
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
public List<string> ExpandedBranchNodesInSideBar
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = [];
|
||||
|
||||
public Dictionary<string, FilterMode> CollectHistoriesFilters()
|
||||
{
|
||||
var map = new Dictionary<string, FilterMode>();
|
||||
|
@ -378,65 +450,13 @@ namespace SourceGit.Models
|
|||
CommitMessages.Insert(0, message);
|
||||
}
|
||||
|
||||
public IssueTrackerRule AddNewIssueTracker()
|
||||
public IssueTrackerRule AddIssueTracker(string name, string regex, string url)
|
||||
{
|
||||
var rule = new IssueTrackerRule()
|
||||
{
|
||||
Name = "New Issue Tracker",
|
||||
RegexString = "#(\\d+)",
|
||||
URLTemplate = "https://xxx/$1",
|
||||
};
|
||||
|
||||
IssueTrackerRules.Add(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
public IssueTrackerRule AddGithubIssueTracker(string repoURL)
|
||||
{
|
||||
var rule = new IssueTrackerRule()
|
||||
{
|
||||
Name = "Github ISSUE",
|
||||
RegexString = "#(\\d+)",
|
||||
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://github.com/username/repository/issues/$1" : $"{repoURL}/issues/$1",
|
||||
};
|
||||
|
||||
IssueTrackerRules.Add(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
public IssueTrackerRule AddJiraIssueTracker()
|
||||
{
|
||||
var rule = new IssueTrackerRule()
|
||||
{
|
||||
Name = "Jira Tracker",
|
||||
RegexString = "PROJ-(\\d+)",
|
||||
URLTemplate = "https://jira.yourcompany.com/browse/PROJ-$1",
|
||||
};
|
||||
|
||||
IssueTrackerRules.Add(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
public IssueTrackerRule AddGitLabIssueTracker(string repoURL)
|
||||
{
|
||||
var rule = new IssueTrackerRule()
|
||||
{
|
||||
Name = "GitLab ISSUE",
|
||||
RegexString = "#(\\d+)",
|
||||
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://gitlab.com/username/repository/-/issues/$1" : $"{repoURL}/-/issues/$1",
|
||||
};
|
||||
|
||||
IssueTrackerRules.Add(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
public IssueTrackerRule AddGitLabMergeRequestTracker(string repoURL)
|
||||
{
|
||||
var rule = new IssueTrackerRule()
|
||||
{
|
||||
Name = "GitLab MR",
|
||||
RegexString = "!(\\d+)",
|
||||
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://gitlab.com/username/repository/-/merge_requests/$1" : $"{repoURL}/-/merge_requests/$1",
|
||||
Name = name,
|
||||
RegexString = regex,
|
||||
URLTemplate = url,
|
||||
};
|
||||
|
||||
IssueTrackerRules.Add(rule);
|
||||
|
@ -451,11 +471,7 @@ namespace SourceGit.Models
|
|||
|
||||
public CustomAction AddNewCustomAction()
|
||||
{
|
||||
var act = new CustomAction()
|
||||
{
|
||||
Name = "Unnamed Custom Action",
|
||||
};
|
||||
|
||||
var act = new CustomAction() { Name = "Unnamed Action" };
|
||||
CustomActions.Add(act);
|
||||
return act;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,6 @@ namespace SourceGit.Models
|
|||
public class RevisionSubmodule
|
||||
{
|
||||
public Commit Commit { get; set; } = null;
|
||||
public string FullMessage { get; set; } = string.Empty;
|
||||
public CommitFullMessage FullMessage { get; set; } = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Reflection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SourceGit.Models
|
||||
|
@ -32,5 +33,24 @@ namespace SourceGit.Models
|
|||
}
|
||||
}
|
||||
|
||||
public class AlreadyUpToDate { }
|
||||
public class AlreadyUpToDate
|
||||
{
|
||||
}
|
||||
|
||||
public class SelfUpdateFailed
|
||||
{
|
||||
public string Reason
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public SelfUpdateFailed(Exception e)
|
||||
{
|
||||
if (e.InnerException is { } inner)
|
||||
Reason = inner.Message;
|
||||
else
|
||||
Reason = e.Message;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,6 +41,8 @@ namespace SourceGit.Models
|
|||
{
|
||||
new ShellOrTerminal("mac-terminal", "Terminal", ""),
|
||||
new ShellOrTerminal("iterm2", "iTerm", ""),
|
||||
new ShellOrTerminal("warp", "Warp", ""),
|
||||
new ShellOrTerminal("ghostty", "Ghostty", "")
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -55,6 +57,7 @@ namespace SourceGit.Models
|
|||
new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"),
|
||||
new ShellOrTerminal("foot", "Foot", "foot"),
|
||||
new ShellOrTerminal("wezterm", "WezTerm", "wezterm"),
|
||||
new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"),
|
||||
new ShellOrTerminal("custom", "Custom", ""),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
|
@ -6,9 +7,10 @@ namespace SourceGit.Models
|
|||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string SHA { get; set; } = "";
|
||||
public List<string> Parents { get; set; } = [];
|
||||
public ulong Time { get; set; } = 0;
|
||||
public string Message { get; set; } = "";
|
||||
|
||||
public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,18 @@
|
|||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public enum TagSortMode
|
||||
{
|
||||
CreatorDate = 0,
|
||||
NameInAscending,
|
||||
NameInDescending,
|
||||
}
|
||||
|
||||
public class Tag : ObservableObject
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string SHA { get; set; } = string.Empty;
|
||||
public ulong CreatorDate { get; set; } = 0;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public FilterMode FilterMode
|
||||
|
|
|
@ -313,7 +313,7 @@ namespace SourceGit.Models
|
|||
|
||||
private static bool IsNameChar(char c)
|
||||
{
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
}
|
||||
|
||||
// (?) notice or log if variable is not found
|
||||
|
|
|
@ -21,10 +21,11 @@ namespace SourceGit.Models
|
|||
{
|
||||
private static readonly ExtraGrammar[] s_extraGrammars =
|
||||
[
|
||||
new ExtraGrammar("source.toml", ".toml", "toml.json"),
|
||||
new ExtraGrammar("source.kotlin", ".kotlin", "kotlin.json"),
|
||||
new ExtraGrammar("source.hx", ".hx", "haxe.json"),
|
||||
new ExtraGrammar("source.hxml", ".hxml", "hxml.json"),
|
||||
new ExtraGrammar("source.toml", [".toml"], "toml.json"),
|
||||
new ExtraGrammar("source.kotlin", [".kotlin", ".kt", ".kts"], "kotlin.json"),
|
||||
new ExtraGrammar("source.hx", [".hx"], "haxe.json"),
|
||||
new ExtraGrammar("source.hxml", [".hxml"], "hxml.json"),
|
||||
new ExtraGrammar("text.html.jsp", [".jsp", ".jspf", ".tag"], "jsp.json"),
|
||||
];
|
||||
|
||||
public static string GetScope(string file, RegistryOptions reg)
|
||||
|
@ -36,13 +37,14 @@ namespace SourceGit.Models
|
|||
extension = ".xml";
|
||||
else if (extension == ".command")
|
||||
extension = ".sh";
|
||||
else if (extension == ".kt" || extension == ".kts")
|
||||
extension = ".kotlin";
|
||||
|
||||
foreach (var grammar in s_extraGrammars)
|
||||
{
|
||||
if (grammar.Extension.Equals(extension, StringComparison.OrdinalIgnoreCase))
|
||||
return grammar.Scope;
|
||||
foreach (var ext in grammar.Extensions)
|
||||
{
|
||||
if (ext.Equals(extension, StringComparison.OrdinalIgnoreCase))
|
||||
return grammar.Scope;
|
||||
}
|
||||
}
|
||||
|
||||
return reg.GetScopeByExtension(extension);
|
||||
|
@ -71,10 +73,10 @@ namespace SourceGit.Models
|
|||
return reg.GetGrammar(scopeName);
|
||||
}
|
||||
|
||||
private record ExtraGrammar(string Scope, string Extension, string File)
|
||||
private record ExtraGrammar(string Scope, List<string> Extensions, string File)
|
||||
{
|
||||
public readonly string Scope = Scope;
|
||||
public readonly string Extension = Extension;
|
||||
public readonly List<string> Extensions = Extensions;
|
||||
public readonly string File = File;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,11 @@ namespace SourceGit.Models
|
|||
return _caches.GetOrAdd(data, key => new User(key));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} <{Email}>";
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<string, User> _caches = new ConcurrentDictionary<string, User>();
|
||||
private readonly int _hash;
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ namespace SourceGit.Models
|
|||
{
|
||||
public class Watcher : IDisposable
|
||||
{
|
||||
public Watcher(IRepository repo)
|
||||
public Watcher(IRepository repo, string fullpath, string gitDir)
|
||||
{
|
||||
_repo = repo;
|
||||
|
||||
_wcWatcher = new FileSystemWatcher();
|
||||
_wcWatcher.Path = _repo.FullPath;
|
||||
_wcWatcher.Path = fullpath;
|
||||
_wcWatcher.Filter = "*";
|
||||
_wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime;
|
||||
_wcWatcher.IncludeSubdirectories = true;
|
||||
|
@ -23,15 +23,8 @@ namespace SourceGit.Models
|
|||
_wcWatcher.Deleted += OnWorkingCopyChanged;
|
||||
_wcWatcher.EnableRaisingEvents = true;
|
||||
|
||||
// If this repository is a worktree repository, just watch the main repository's gitdir.
|
||||
var gitDirNormalized = _repo.GitDir.Replace("\\", "/");
|
||||
var worktreeIdx = gitDirNormalized.IndexOf(".git/worktrees/", StringComparison.Ordinal);
|
||||
var repoWatchDir = _repo.GitDir;
|
||||
if (worktreeIdx > 0)
|
||||
repoWatchDir = _repo.GitDir.Substring(0, worktreeIdx + 4);
|
||||
|
||||
_repoWatcher = new FileSystemWatcher();
|
||||
_repoWatcher.Path = repoWatchDir;
|
||||
_repoWatcher.Path = gitDir;
|
||||
_repoWatcher.Filter = "*";
|
||||
_repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;
|
||||
_repoWatcher.IncludeSubdirectories = true;
|
||||
|
@ -72,6 +65,11 @@ namespace SourceGit.Models
|
|||
_updateBranch = DateTime.Now.ToFileTime() - 1;
|
||||
}
|
||||
|
||||
public void MarkTagDirtyManually()
|
||||
{
|
||||
_updateTags = DateTime.Now.ToFileTime() - 1;
|
||||
}
|
||||
|
||||
public void MarkWorkingCopyDirtyManually()
|
||||
{
|
||||
_updateWC = DateTime.Now.ToFileTime() - 1;
|
||||
|
@ -109,6 +107,7 @@ namespace SourceGit.Models
|
|||
{
|
||||
_updateBranch = 0;
|
||||
_updateWC = 0;
|
||||
_updateSubmodules = 0;
|
||||
|
||||
if (_updateTags > 0)
|
||||
{
|
||||
|
@ -119,6 +118,7 @@ namespace SourceGit.Models
|
|||
Task.Run(_repo.RefreshBranches);
|
||||
Task.Run(_repo.RefreshCommits);
|
||||
Task.Run(_repo.RefreshWorkingCopyChanges);
|
||||
Task.Run(_repo.RefreshSubmodules);
|
||||
Task.Run(_repo.RefreshWorktrees);
|
||||
}
|
||||
|
||||
|
@ -131,20 +131,20 @@ namespace SourceGit.Models
|
|||
if (_updateSubmodules > 0 && now > _updateSubmodules)
|
||||
{
|
||||
_updateSubmodules = 0;
|
||||
_repo.RefreshSubmodules();
|
||||
Task.Run(_repo.RefreshSubmodules);
|
||||
}
|
||||
|
||||
if (_updateStashes > 0 && now > _updateStashes)
|
||||
{
|
||||
_updateStashes = 0;
|
||||
_repo.RefreshStashes();
|
||||
Task.Run(_repo.RefreshStashes);
|
||||
}
|
||||
|
||||
if (_updateTags > 0 && now > _updateTags)
|
||||
{
|
||||
_updateTags = 0;
|
||||
_repo.RefreshTags();
|
||||
_repo.RefreshCommits();
|
||||
Task.Run(_repo.RefreshTags);
|
||||
Task.Run(_repo.RefreshCommits);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,12 +173,6 @@ namespace SourceGit.Models
|
|||
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
|
||||
{
|
||||
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
|
||||
lock (_submodules)
|
||||
{
|
||||
if (_submodules.Count > 0)
|
||||
_updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
}
|
||||
}
|
||||
else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal))
|
||||
{
|
||||
|
@ -195,7 +189,7 @@ namespace SourceGit.Models
|
|||
if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
lock (_submodules)
|
||||
lock (_lockSubmodule)
|
||||
{
|
||||
foreach (var submodule in _submodules)
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace SourceGit.Models
|
|||
{
|
||||
public string Branch { get; set; } = string.Empty;
|
||||
public string FullPath { get; set; } = string.Empty;
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public string Head { get; set; } = string.Empty;
|
||||
public bool IsBare { get; set; } = false;
|
||||
public bool IsDetached { get; set; } = false;
|
||||
|
@ -21,15 +22,15 @@ namespace SourceGit.Models
|
|||
get
|
||||
{
|
||||
if (IsDetached)
|
||||
return $"(deteched HEAD at {Head.Substring(10)})";
|
||||
return $"deteched HEAD at {Head.Substring(10)}";
|
||||
|
||||
if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
|
||||
return $"({Branch.Substring(11)})";
|
||||
return Branch.Substring(11);
|
||||
|
||||
if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
|
||||
return $"({Branch.Substring(13)})";
|
||||
return Branch.Substring(13);
|
||||
|
||||
return $"({Branch})";
|
||||
return Branch;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,13 +65,16 @@ namespace SourceGit.Native
|
|||
{
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var cwd = string.IsNullOrEmpty(workdir) ? home : workdir;
|
||||
var terminal = OS.ShellOrTerminal;
|
||||
|
||||
var startInfo = new ProcessStartInfo();
|
||||
startInfo.WorkingDirectory = cwd;
|
||||
startInfo.FileName = OS.ShellOrTerminal;
|
||||
startInfo.FileName = terminal;
|
||||
|
||||
if (OS.ShellOrTerminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase))
|
||||
if (terminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase))
|
||||
startInfo.Arguments = $"start --cwd \"{cwd}\"";
|
||||
else if (terminal.EndsWith("ptyxis", StringComparison.OrdinalIgnoreCase))
|
||||
startInfo.Arguments = $"--new-window --working-directory=\"{cwd}\"";
|
||||
|
||||
try
|
||||
{
|
||||
|
|
|
@ -18,14 +18,33 @@ namespace SourceGit.Native
|
|||
DisableDefaultApplicationMenuItems = true,
|
||||
});
|
||||
|
||||
// Fix `PATH` env on macOS.
|
||||
var path = Environment.GetEnvironmentVariable("PATH");
|
||||
if (string.IsNullOrEmpty(path))
|
||||
path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
||||
else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal))
|
||||
path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path;
|
||||
|
||||
var customPathFile = Path.Combine(OS.DataDir, "PATH");
|
||||
if (File.Exists(customPathFile))
|
||||
OS.CustomPathEnv = File.ReadAllText(customPathFile).Trim();
|
||||
{
|
||||
var env = File.ReadAllText(customPathFile).Trim();
|
||||
if (!string.IsNullOrEmpty(env))
|
||||
path = env;
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("PATH", path);
|
||||
}
|
||||
|
||||
public string FindGitExecutable()
|
||||
{
|
||||
return File.Exists("/usr/bin/git") ? "/usr/bin/git" : string.Empty;
|
||||
var gitPathVariants = new List<string>() {
|
||||
"/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git", "/opt/homebrew/opt/git/bin/git"
|
||||
};
|
||||
foreach (var path in gitPathVariants)
|
||||
if (File.Exists(path))
|
||||
return path;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string FindTerminal(Models.ShellOrTerminal shell)
|
||||
|
@ -36,6 +55,10 @@ namespace SourceGit.Native
|
|||
return "Terminal";
|
||||
case "iterm2":
|
||||
return "iTerm";
|
||||
case "warp":
|
||||
return "Warp";
|
||||
case "ghostty":
|
||||
return "Ghostty";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
|
125
src/Native/OS.cs
125
src/Native/OS.cs
|
@ -1,12 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Avalonia;
|
||||
|
||||
namespace SourceGit.Native
|
||||
{
|
||||
public static class OS
|
||||
public static partial class OS
|
||||
{
|
||||
public interface IBackend
|
||||
{
|
||||
|
@ -22,11 +25,48 @@ namespace SourceGit.Native
|
|||
void OpenWithDefaultEditor(string file);
|
||||
}
|
||||
|
||||
public static string DataDir { get; private set; } = string.Empty;
|
||||
public static string GitExecutable { get; set; } = string.Empty;
|
||||
public static string ShellOrTerminal { get; set; } = string.Empty;
|
||||
public static List<Models.ExternalTool> ExternalTools { get; set; } = [];
|
||||
public static string CustomPathEnv { get; set; } = string.Empty;
|
||||
public static string DataDir
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = string.Empty;
|
||||
|
||||
public static string GitExecutable
|
||||
{
|
||||
get => _gitExecutable;
|
||||
set
|
||||
{
|
||||
if (_gitExecutable != value)
|
||||
{
|
||||
_gitExecutable = value;
|
||||
UpdateGitVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string GitVersionString
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = string.Empty;
|
||||
|
||||
public static Version GitVersion
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new Version(0, 0, 0);
|
||||
|
||||
public static string ShellOrTerminal
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = string.Empty;
|
||||
|
||||
public static List<Models.ExternalTool> ExternalTools
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = [];
|
||||
|
||||
static OS()
|
||||
{
|
||||
|
@ -55,6 +95,17 @@ namespace SourceGit.Native
|
|||
|
||||
public static void SetupDataDir()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var execFile = Process.GetCurrentProcess().MainModule!.FileName;
|
||||
var portableDir = Path.Combine(Path.GetDirectoryName(execFile), "data");
|
||||
if (Directory.Exists(portableDir))
|
||||
{
|
||||
DataDir = portableDir;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var osAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
if (string.IsNullOrEmpty(osAppDataDir))
|
||||
DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit");
|
||||
|
@ -111,6 +162,68 @@ namespace SourceGit.Native
|
|||
_backend.OpenWithDefaultEditor(file);
|
||||
}
|
||||
|
||||
public static string GetAbsPath(string root, string sub)
|
||||
{
|
||||
var fullpath = Path.Combine(root, sub);
|
||||
if (OperatingSystem.IsWindows())
|
||||
return fullpath.Replace('/', '\\');
|
||||
|
||||
return fullpath;
|
||||
}
|
||||
|
||||
private static void UpdateGitVersion()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable))
|
||||
{
|
||||
GitVersionString = string.Empty;
|
||||
GitVersion = new Version(0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = _gitExecutable;
|
||||
start.Arguments = "--version";
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.RedirectStandardOutput = true;
|
||||
start.RedirectStandardError = true;
|
||||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
var proc = new Process() { StartInfo = start };
|
||||
try
|
||||
{
|
||||
proc.Start();
|
||||
|
||||
var rs = proc.StandardOutput.ReadToEnd();
|
||||
proc.WaitForExit();
|
||||
if (proc.ExitCode == 0 && !string.IsNullOrWhiteSpace(rs))
|
||||
{
|
||||
GitVersionString = rs.Trim();
|
||||
|
||||
var match = REG_GIT_VERSION().Match(GitVersionString);
|
||||
if (match.Success)
|
||||
{
|
||||
var major = int.Parse(match.Groups[1].Value);
|
||||
var minor = int.Parse(match.Groups[2].Value);
|
||||
var build = int.Parse(match.Groups[3].Value);
|
||||
GitVersion = new Version(major, minor, build);
|
||||
GitVersionString = GitVersionString.Substring(11).Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
proc.Close();
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^git version[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")]
|
||||
private static partial Regex REG_GIT_VERSION();
|
||||
|
||||
private static IBackend _backend = null;
|
||||
private static string _gitExecutable = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SourceGit.Native
|
||||
{
|
||||
|
@ -26,9 +27,21 @@ namespace SourceGit.Native
|
|||
internal string szCSDVersion;
|
||||
}
|
||||
|
||||
[DllImport("ntdll")]
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MARGINS
|
||||
{
|
||||
public int cxLeftWidth;
|
||||
public int cxRightWidth;
|
||||
public int cyTopHeight;
|
||||
public int cyBottomHeight;
|
||||
}
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);
|
||||
|
||||
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
|
||||
|
||||
|
@ -140,7 +153,7 @@ namespace SourceGit.Native
|
|||
|
||||
public void OpenBrowser(string url)
|
||||
{
|
||||
var info = new ProcessStartInfo("cmd", $"/c start {url}");
|
||||
var info = new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\"");
|
||||
info.CreateNoWindow = true;
|
||||
Process.Start(info);
|
||||
}
|
||||
|
@ -202,10 +215,17 @@ namespace SourceGit.Native
|
|||
|
||||
private void FixWindowFrameOnWin10(Window w)
|
||||
{
|
||||
if (w.WindowState == WindowState.Maximized || w.WindowState == WindowState.FullScreen)
|
||||
w.SystemDecorations = SystemDecorations.Full;
|
||||
else if (w.WindowState == WindowState.Normal)
|
||||
w.SystemDecorations = SystemDecorations.BorderOnly;
|
||||
// Schedule the DWM frame extension to run in the next render frame
|
||||
// to ensure proper timing with the window initialization sequence
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var platformHandle = w.TryGetPlatformHandle();
|
||||
if (platformHandle == null)
|
||||
return;
|
||||
|
||||
var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 };
|
||||
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
|
||||
}, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
#region EXTERNAL_EDITOR_FINDER
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"information_for_contributors": [
|
||||
"This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/haxe.tmLanguage",
|
||||
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage"
|
||||
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage",
|
||||
"The original file was licensed under the MIT License",
|
||||
"https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md"
|
||||
],
|
||||
"fileTypes": [
|
||||
"hx",
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"information_for_contributors": [
|
||||
"This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/hxml.tmLanguage",
|
||||
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage"
|
||||
"and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage",
|
||||
"The original file was licensed under the MIT License",
|
||||
"https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md"
|
||||
],
|
||||
"fileTypes": [
|
||||
"hxml"
|
||||
|
|
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