Compare commits

..

No commits in common. "master" and "v8.35" have entirely different histories.

476 changed files with 10908 additions and 30479 deletions

View file

@ -100,7 +100,7 @@ dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# use accessibility modifiers
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
@ -206,9 +206,6 @@ dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
# IDE0005: remove used namespace using
dotnet_diagnostic.IDE0005.severity = error
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
@ -295,12 +292,3 @@ indent_size = 2
end_of_line = lf
[*.{cmd,bat}]
end_of_line = crlf
# Package manifests
[{*.spec,control}]
end_of_line = lf
# YAML files
[*.{yml,yaml}]
indent_size = 2
end_of_line = lf

4
.gitattributes vendored
View file

@ -3,12 +3,10 @@
*.png binary
*.ico binary
*.sh text eol=lf
*.spec text eol=lf
control text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.json text
.gitattributes export-ignore
.gitignore export-ignore
.gitignore export-ignore

View file

@ -19,31 +19,20 @@ jobs:
os: macos-latest
runtime: osx-arm64
- name : Linux
os: ubuntu-latest
os: ubuntu-20.04
runtime: linux-x64
container: ubuntu:20.04
- name : Linux (arm64)
os: ubuntu-latest
os: ubuntu-20.04
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
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Configure arm64 packages
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
@ -58,7 +47,7 @@ jobs:
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
sudo apt-get update
sudo apt-get install -y llvm gcc-aarch64-linux-gnu
sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
- name: Build
run: dotnet build -c Release
- name: Publish

View file

@ -1,41 +0,0 @@
name: Localization Check
on:
push:
branches: [ develop ]
paths:
- 'src/Resources/Locales/**'
workflow_dispatch:
workflow_call:
jobs:
localization-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies
run: npm install fs-extra@11.2.0 path@0.12.7 xml2js@0.6.2
- name: Run localization check
run: node build/scripts/localization-check.js
- name: Commit changes
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
if [ -n "$(git status --porcelain)" ]; then
git add TRANSLATION.md src/Resources/Locales/*.axaml
git commit -m 'doc: Update translation status and sort locale files'
git push
else
echo "No changes to commit"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -3,16 +3,16 @@ on:
workflow_call:
inputs:
version:
description: SourceGit package version
description: Source Git package version
required: true
type: string
jobs:
windows:
name: Package Windows
runs-on: windows-2019
windows-portable:
name: Package portable Windows app
runs-on: ubuntu-latest
strategy:
matrix:
runtime: [ win-x64, win-arm64 ]
runtime: [win-x64, win-arm64]
steps:
- name: Checkout sources
uses: actions/checkout@v4
@ -22,11 +22,10 @@ jobs:
name: sourcegit.${{ matrix.runtime }}
path: build/SourceGit
- name: Package
shell: bash
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
run: ./build/scripts/package.windows.sh
run: ./build/scripts/package.windows-portable.sh
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
@ -37,7 +36,7 @@ jobs:
with:
name: sourcegit.${{ matrix.runtime }}
osx-app:
name: Package macOS
name: Package OSX app
runs-on: macos-latest
strategy:
matrix:
@ -70,7 +69,6 @@ jobs:
linux:
name: Package Linux
runs-on: ubuntu-latest
container: ubuntu:20.04
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
@ -79,10 +77,9 @@ jobs:
uses: actions/checkout@v4
- name: Download package dependencies
run: |
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
sudo add-apt-repository universe
sudo apt-get update
sudo apt-get install desktop-file-utils rpm libfuse2
- name: Download build
uses: actions/download-artifact@v4
with:
@ -92,7 +89,6 @@ jobs:
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
APPIMAGE_EXTRACT_AND_RUN: 1
run: |
mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit

View file

@ -38,7 +38,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.version.outputs.version }}
run: gh release create "$TAG" -t "$VERSION" --notes-from-tag
run: gh release create "$TAG" -t "Release $VERSION" --notes-from-tag
- name: Download artifacts
uses: actions/download-artifact@v4
with:

8
.gitignore vendored
View file

@ -23,10 +23,6 @@ ehthumbs_vista.db
bin/
obj/
# ignore ci node files
node_modules/
package.json
package-lock.json
build/resources/
build/SourceGit/
@ -36,6 +32,4 @@ build/*.tar.gz
build/*.deb
build/*.rpm
build/*.AppImage
SourceGit.app/
build.command
src/Properties/launchSettings.json
SourceGit.app/

View file

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

120
README.md
View file

@ -1,26 +1,22 @@
# SourceGit - Opensource Git GUI client.
# SourceGit
[![stars](https://img.shields.io/github/stars/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/stargazers)
[![forks](https://img.shields.io/github/forks/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/forks)
[![license](https://img.shields.io/github/license/sourcegit-scm/sourcegit.svg)](LICENSE)
[![latest](https://img.shields.io/github/v/release/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/releases/latest)
[![downloads](https://img.shields.io/github/downloads/sourcegit-scm/sourcegit/total)](https://github.com/sourcegit-scm/sourcegit/releases)
Opensource Git GUI client.
## Highlights
* Supports Windows/macOS/Linux
* Opensource/Free
* Fast
* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)
* English/Français/Deutsch/Português/Русский/简体中文/繁體中文
* Built-in light/dark themes
* Customize theme
* Visual commit graph
* Supports SSH access with each remote
* GIT commands with GUI
* Clone/Fetch/Pull/Push...
* Merge/Rebase/Reset/Revert/Cherry-pick...
* Amend/Reword/Squash
* Interactive rebase
* Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
* Amend/Reword
* Interactive rebase (Basic)
* Branches
* Remotes
* Tags
@ -35,40 +31,32 @@
* Revision Diffs
* Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend
* Git command logs
* Search commits
* GitFlow
* Git LFS
* Bisect
* Issue Link
* Workspace
* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING]
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.
## Translation Status
You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md)
## How to Use
**To use this tool, you need to install Git(>=2.25.1) first.**
**To use this tool, you need to install Git(>=2.23.0) first.**
You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [Github Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
| OS | PATH |
|---------|-----------------------------------------------------|
| Windows | `%APPDATA%\SourceGit` |
| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` |
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
| macOS | `${HOME}/Library/Application Support/SourceGit` |
> [!TIP]
> * 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.
> You can open the app data dir from the main menu.
For **Windows** users:
@ -79,91 +67,60 @@ 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 `scoop` with follow commands:
* You can install the latest stable by `scoope` with follow commands:
```shell
scoop bucket add extras
scoop install sourcegit
```
* Pre-built binaries can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest)
* Portable versions can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest)
For **macOS** users:
* Thanks [@ybeapps](https://github.com/ybeapps) for making `SourceGit` available on `Homebrew`. You can simply install it with following command:
```shell
brew tap ybeapps/homebrew-sourcegit
brew install --cask --no-quarantine sourcegit
```
* If you want to install `SourceGit.app` from GitHub Release manually, you need run following command to make sure it works:
```shell
sudo xattr -cr /Applications/SourceGit.app
```
* Download `sourcegit_x.y.osx-x64.zip` or `sourcegit_x.y.osx-arm64.zip` from Releases. `x64` for Intel and `arm64` for Apple Silicon.
* Move `SourceGit.app` to `Applications` folder.
* Make sure your mac trusts all software from anywhere. For more information, search `spctl --master-disable`.
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your mac.
* You can run `echo $PATH > ~/Library/Application\ Support/SourceGit/PATH` to generate a custom PATH env file to introduce `PATH` env to SourceGit.
* You may need to run `sudo xattr -cr /Applications/SourceGit.app` to make sure the software works.
For **Linux** users:
* Thanks [@aikawayataro](https://github.com/aikawayataro) for providing `rpm` and `deb` repositories, hosted on [Codeberg](https://codeberg.org/yataro/-/packages).
`deb` how to:
```shell
curl https://codeberg.org/api/packages/yataro/debian/repository.key | sudo tee /etc/apt/keyrings/sourcegit.asc
echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc, 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
```
`rpm` how to:
```shell
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
```
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.
* `xdg-open` must be installed to support open native file manager.
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux.
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
* 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 compatible 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 comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window.
For `OpenAI`:
* `Server` must be `https://api.openai.com/v1`
* `Server` must be `https://api.openai.com/v1/chat/completions`
For other AI service:
* 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 `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 `API Key` is optional that depends on the service
## External Tools
This app supports open repository in external tools listed in the table below.
| Tool | Windows | macOS | Linux |
|-------------------------------|---------|-------|-------|
| Visual Studio Code | YES | YES | YES |
| Visual Studio Code - Insiders | YES | YES | YES |
| VSCodium | YES | YES | YES |
| Fleet | YES | YES | YES |
| Sublime Text | YES | YES | YES |
| Zed | NO | YES | YES |
| Visual Studio | YES | NO | NO |
| Tool | Windows | macOS | Linux | KEY IN `external_editors.json` |
|-------------------------------|---------|-------|-------|--------------------------------|
| Visual Studio Code | YES | YES | YES | VSCODE |
| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS |
| VSCodium | YES | YES | YES | VSCODIUM |
| JetBrains Fleet | YES | YES | YES | FLEET |
| Sublime Text | YES | YES | YES | SUBLIME_TEXT |
| Zed | NO | YES | YES | ZED |
> [!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 storage directory and provide the path directly. For example:
> To solve this problem you can add a file named `external_editors.json` in app data dir and provide the path directly. For example:
```json
{
"tools": {
"Visual Studio Code": "D:\\VSCode\\Code.exe"
"VSCODE": "D:\\VSCode\\Code.exe"
}
}
```
@ -189,19 +146,6 @@ This app supports open repository in external tools listed in the table below.
Everyone is welcome to submit a PR. Please make sure your PR is based on the latest `develop` branch and the target branch of PR is `develop`.
In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download):
```sh
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
dotnet restore
dotnet build
dotnet run --project src/SourceGit.csproj
```
Thanks to all the people who contribute.
[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=20)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
## Third-Party Components
For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md).
[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=10)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)

View file

@ -13,11 +13,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F45A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{67B6D05F-A000-40BA-ADB4-C9065F880D7B}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\ci.yml = .github\workflows\ci.yml
.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
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}"
@ -60,8 +58,6 @@ 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}"
@ -79,10 +75,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "appimage", "appimage", "{5D
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C54D4001-9940-477C-A0B6-E795ED0A3209}"
ProjectSection(SolutionItems) = preProject
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.sh = build\scripts\package.windows.sh
build\scripts\package.windows-portable.sh = build\scripts\package.windows-portable.sh
EndProjectSection
EndProject
Global

View file

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

View file

@ -1,511 +0,0 @@
# Translation Status
This document shows the translation status of each locale file in the repository.
## Details
### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)
### ![de__DE](https://img.shields.io/badge/de__DE-96.14%25-yellow)
<details>
<summary>Missing keys in de_DE.axaml</summary>
- Text.Avatar.Load
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitDetail.Changes.Count
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Pull.RecurseSubmodules
- Text.Repository.ClearStashes
- Text.Repository.ShowSubmodulesAsTree
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.WorkingCopy.ResetAuthor
</details>
### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen)
### ![fr__FR](https://img.shields.io/badge/fr__FR-92.03%25-yellow)
<details>
<summary>Missing keys in fr_FR.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![it__IT](https://img.shields.io/badge/it__IT-97.38%25-yellow)
<details>
<summary>Missing keys in it_IT.axaml</summary>
- Text.Avatar.Load
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitDetail.Changes.Count
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Pull.RecurseSubmodules
- Text.Repository.ClearStashes
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Submodule.Deinit
- Text.WorkingCopy.ResetAuthor
</details>
### ![ja__JP](https://img.shields.io/badge/ja__JP-91.78%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CompareWithCurrent
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.FilterCommits
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-83.81%25-yellow)
<details>
<summary>Missing keys in pt_BR.axaml</summary>
- Text.AIAssistant.Regen
- Text.AIAssistant.Use
- Text.ApplyStash
- Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
- Text.BranchCM.ResetToSelectedCommit
- Text.BranchUpstreamInvalid
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.Clone.RecurseSubmodules
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitDetail.Changes.Count
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.CustomAction.Scope.Branch
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.Git.PreferredMergeMode
- Text.Configure.IssueTracker.AddSampleGiteeIssue
- Text.Configure.IssueTracker.AddSampleGiteePullRequest
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.CreateBranch.Name.WarnSpace
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.DeleteRepositoryNode.Path
- Text.DeleteRepositoryNode.TipForGroup
- Text.DeleteRepositoryNode.TipForRepository
- Text.Diff.First
- Text.Diff.Last
- Text.Diff.Submodule.Deleted
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.Clone
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Source
- Text.MergeMultiple
- Text.MergeMultiple.CommitChanges
- Text.MergeMultiple.Strategy
- Text.MergeMultiple.Targets
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Preferences.Git.SSLVerify
- Text.Pull.RecurseSubmodules
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.FilterCommits
- Text.Repository.HistoriesLayout
- Text.Repository.HistoriesLayout.Horizontal
- Text.Repository.HistoriesLayout.Vertical
- Text.Repository.HistoriesOrder
- Text.Repository.Notifications.Clear
- Text.Repository.OnlyHighlightCurrentBranchInHistories
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.Skip
- Text.Repository.Tags.OrderByCreatorDate
- Text.Repository.Tags.OrderByName
- Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- 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.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
- Text.WorkingCopy.SignOff
</details>
### ![ru__RU](https://img.shields.io/badge/ru__RU-99.75%25-yellow)
<details>
<summary>Missing keys in ru_RU.axaml</summary>
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
</details>
### ![ta__IN](https://img.shields.io/badge/ta__IN-91.91%25-yellow)
<details>
<summary>Missing keys in ta_IN.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CompareWithCurrent
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.UpdateSubmodules.Target
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![uk__UA](https://img.shields.io/badge/uk__UA-93.15%25-yellow)
<details>
<summary>Missing keys in uk_UA.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.ConfigureWorkspace.Name
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.ResetAuthor
</details>
### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)
### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)

View file

@ -1 +1 @@
2025.22
8.35

View file

@ -5,11 +5,11 @@
## How to build this project manually
1. Make sure [.NET SDK 9](https://dotnet.microsoft.com/en-us/download) is installed on your machine.
1. Make sure [.NET SDK 8](https://dotnet.microsoft.com/en-us/download) is installed on your machine.
2. Clone this project
3. Run the follow command under the project root dir
```sh
dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj
```
> [!NOTE]
> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replace the `$DESTINATION_FOLDER` with the real path that will store the output executable files.
> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replece the `$DESTINATION_FOLDER` with the real path that will store the output executable files.

View file

@ -1,5 +1,5 @@
[Desktop Entry]
Name=SourceGit
Name=Source Git
Comment=Open-source & Free Git GUI Client
Exec=/opt/sourcegit/sourcegit
Icon=/usr/share/icons/sourcegit.png

View file

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

View file

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

View file

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

View file

@ -5,10 +5,8 @@ Summary: Open-source & Free Git Gui Client
License: MIT
URL: https://sourcegit-scm.github.io/
Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.tar.gz
Requires: libX11.so.6()(%{__isa_bits}bit)
Requires: libSM.so.6()(%{__isa_bits}bit)
Requires: libicu
Requires: xdg-utils
Requires: libX11
Requires: libSM
%define _build_id_links none

View file

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

View file

@ -5,6 +5,16 @@ set -o
set -u
set pipefail
if [[ -z "$VERSION" ]]; then
echo "Provide the version as environment variable VERSION"
exit 1
fi
if [[ -z "$RUNTIME" ]]; then
echo "Provide the runtime as environment variable RUNTIME"
exit 1
fi
arch=
appimage_arch=
target=
@ -56,15 +66,8 @@ 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
# 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"
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"
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./

View file

@ -5,6 +5,16 @@ set -o
set -u
set pipefail
if [[ -z "$VERSION" ]]; then
echo "Provide the version as environment variable VERSION"
exit 1
fi
if [[ -z "$RUNTIME" ]]; then
echo "Provide the runtime as environment variable RUNTIME"
exit 1
fi
cd build
mkdir -p SourceGit.app/Contents/Resources

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -e
set -o
set -u
set pipefail
if [[ -z "$VERSION" ]]; then
echo "Provide the version as environment variable VERSION"
exit 1
fi
if [[ -z "$RUNTIME" ]]; then
echo "Provide the runtime as environment variable RUNTIME"
exit 1
fi
cd build
rm -rf SourceGit/*.pdb
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit

View file

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

View file

@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.0",
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": false
}

View file

@ -25,34 +25,12 @@ namespace SourceGit
private Action<object> _action = null;
}
public static bool IsCheckForUpdateCommandVisible
{
get
{
#if DISABLE_UPDATE_DETECTION
return false;
#else
return true;
#endif
}
}
public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false));
public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false));
public static readonly Command OpenPreferenceCommand = new Command(_ => OpenDialog(new Views.Preference()));
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(_ => ShowWindow(new Views.About(), false));
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true));
public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
public static readonly Command CheckForUpdateCommand = new Command(_ => Check4Update(true));
public static readonly Command QuitCommand = new Command(_ => Quit(0));
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);
});
public static readonly Command CopyTextBlockCommand = new Command(p => CopyTextBlock(p as TextBlock));
}
}

View file

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

View file

@ -14,15 +14,10 @@
<ResourceInclude x:Key="de_DE" Source="/Resources/Locales/de_DE.axaml"/>
<ResourceInclude x:Key="en_US" Source="/Resources/Locales/en_US.axaml"/>
<ResourceInclude x:Key="fr_FR" Source="/Resources/Locales/fr_FR.axaml"/>
<ResourceInclude x:Key="it_IT" Source="/Resources/Locales/it_IT.axaml"/>
<ResourceInclude x:Key="pt_BR" Source="/Resources/Locales/pt_BR.axaml"/>
<ResourceInclude x:Key="uk_UA" Source="/Resources/Locales/uk_UA.axaml"/>
<ResourceInclude x:Key="ru_RU" Source="/Resources/Locales/ru_RU.axaml"/>
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/>
<ResourceInclude x:Key="zh_TW" Source="/Resources/Locales/zh_TW.axaml"/>
<ResourceInclude x:Key="es_ES" Source="/Resources/Locales/es_ES.axaml"/>
<ResourceInclude x:Key="ja_JP" Source="/Resources/Locales/ja_JP.axaml"/>
<ResourceInclude x:Key="ta_IN" Source="/Resources/Locales/ta_IN.axaml"/>
</ResourceDictionary>
</Application.Resources>
@ -35,10 +30,10 @@
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" Gesture="F1"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.Preference}" Command="{x:Static s:App.OpenPreferenceCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>

View file

@ -1,13 +1,10 @@
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.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
@ -25,7 +22,6 @@ namespace SourceGit
{
public partial class App : Application
{
#region App Entry Point
[STAThread]
public static void Main(string[] args)
{
@ -38,14 +34,15 @@ namespace SourceGit
TaskScheduler.UnobservedTaskException += (_, e) =>
{
LogException(e.Exception);
e.SetObserved();
};
try
{
if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo))
if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo))
Environment.Exit(exitTodo);
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage))
Environment.Exit(exitMessage);
else
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
@ -62,10 +59,6 @@ namespace SourceGit
builder.UsePlatformDetect();
builder.LogToTrace();
builder.WithInterFont();
builder.With(new FontManagerOptions()
{
DefaultFamilyName = "fonts:Inter#Inter"
});
builder.ConfigureFonts(manager =>
{
var monospace = new EmbeddedFontCollection(
@ -78,71 +71,38 @@ namespace SourceGit
return builder;
}
public static void LogException(Exception ex)
public override void Initialize()
{
if (ex == null)
return;
AvaloniaXamlLoader.Load(this);
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
builder.Append($"User: {Environment.UserName}\n");
builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
builder.Append($"Exception Time: {DateTime.Now}\n");
builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex);
var pref = ViewModels.Preference.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
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());
SetLocale(pref.Locale);
SetTheme(pref.Theme, pref.ThemeOverrides);
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
}
#endregion
#region Utility Functions
public static void ShowWindow(object data, bool showAsDialog)
public override void OnFrameworkInitializationCompleted()
{
var impl = (Views.ChromelessWindow target, bool isDialog) =>
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
if (isDialog)
target.ShowDialog(owner);
else
target.Show(owner);
}
else
{
target.Show();
}
};
BindingPlugins.DataValidators.RemoveAt(0);
if (data is Views.ChromelessWindow window)
{
impl(window, showAsDialog);
return;
if (TryLaunchedAsCoreEditor(desktop))
return;
if (TryLaunchedAsAskpass(desktop))
return;
TryLaunchedAsNormal(desktop);
}
}
var dataTypeName = data.GetType().FullName;
if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal))
return;
var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
var viewType = Type.GetType(viewTypeName);
if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow)))
return;
window = Activator.CreateInstance(viewType) as Views.ChromelessWindow;
if (window != null)
{
window.DataContext = data;
impl(window, showAsDialog);
}
public static void OpenDialog(Window window)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
window.ShowDialog(owner);
}
public static void RaiseException(string context, string message)
@ -240,9 +200,6 @@ 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));
@ -266,7 +223,7 @@ namespace SourceGit
if (onlyUseMonospaceFontInEditor)
{
if (string.IsNullOrEmpty(defaultFont))
resDic.Add("Fonts.Primary", new FontFamily("fonts:Inter#Inter"));
resDic.Add("Fonts.Primary", new FontFamily("fonts:Inter#Inter, $Default"));
else
resDic.Add("Fonts.Primary", new FontFamily(defaultFont));
}
@ -301,7 +258,7 @@ namespace SourceGit
return await clipboard.GetTextAsync();
}
}
return null;
return default;
}
public static string Text(string key, params object[] args)
@ -323,7 +280,8 @@ namespace SourceGit
icon.Height = 12;
icon.Stretch = Stretch.Uniform;
if (Current?.FindResource(key) is StreamGeometry geo)
var geo = Current?.FindResource(key) as StreamGeometry;
if (geo != null)
icon.Data = geo;
return icon;
@ -337,11 +295,26 @@ namespace SourceGit
return null;
}
public static ViewModels.Launcher GetLauncher()
public static ViewModels.Launcher GetLauncer()
{
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)
@ -354,68 +327,94 @@ namespace SourceGit
Environment.Exit(exitCode);
}
}
#endregion
#region Overrides
public override void Initialize()
private static void CopyTextBlock(TextBlock textBlock)
{
AvaloniaXamlLoader.Load(this);
if (textBlock == null)
return;
var pref = ViewModels.Preferences.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
SetLocale(pref.Locale);
SetTheme(pref.Theme, pref.ThemeOverrides);
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
if (textBlock.Inlines is { Count: > 0 } inlines)
CopyText(inlines.Text);
else if (!string.IsNullOrEmpty(textBlock.Text))
CopyText(textBlock.Text);
}
public override void OnFrameworkInitializationCompleted()
private static void LogException(Exception ex)
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
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)
{
BindingPlugins.DataValidators.RemoveAt(0);
ex = ex.InnerException;
builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n");
builder.Append(ex.StackTrace);
}
// Disable tooltip if window is not active.
ToolTip.ToolTipOpeningEvent.AddClassHandler<Control>((c, e) =>
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
File.WriteAllText(file, builder.ToString());
}
private static void Check4Update(bool manually = false)
{
Task.Run(async () =>
{
try
{
var topLevel = TopLevel.GetTopLevel(c);
if (topLevel is not Window { IsActive: true })
e.Cancel = true;
});
// 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");
if (TryLaunchAsCoreEditor(desktop))
return;
// Parse json into Models.Version.
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
if (ver == null)
return;
if (TryLaunchAsAskpass(desktop))
return;
_ipcChannel = new Models.IpcChannel();
if (!_ipcChannel.IsFirstInstance)
{
var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty;
if (!string.IsNullOrEmpty(arg))
// Check if already up-to-date.
if (!ver.IsNewVersion)
{
if (arg.StartsWith('"') && arg.EndsWith('"'))
arg = arg.Substring(1, arg.Length - 2).Trim();
if (arg.Length > 0 && !Path.IsPathFullyQualified(arg))
arg = Path.GetFullPath(arg);
if (manually)
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
return;
}
_ipcChannel.SendToFirstInstance(arg);
Environment.Exit(0);
}
else
{
_ipcChannel.MessageReceived += TryOpenRepository;
desktop.Exit += (_, _) => _ipcChannel.Dispose();
TryLaunchAsNormal(desktop);
}
}
}
#endregion
// Should not check ignored tag if this is called manually.
if (!manually)
{
var pref = ViewModels.Preference.Instance;
if (ver.TagName == pref.IgnoreUpdateTag)
return;
}
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
ShowSelfUpdateResult(ver);
}
catch (Exception e)
{
if (manually)
ShowSelfUpdateResult(e);
}
});
}
private static void ShowSelfUpdateResult(object data)
{
Dispatcher.UIThread.Post(() =>
{
OpenDialog(new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } });
});
}
private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -468,57 +467,39 @@ namespace SourceGit
return true;
}
private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCode)
private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode)
{
exitCode = -1;
if (args.Length <= 1 || !args[0].Equals("--rebase-message-editor", StringComparison.Ordinal))
return false;
exitCode = 0;
var file = args[1];
var filename = Path.GetFileName(file);
if (!filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase))
return true;
var gitDir = Path.GetDirectoryName(file)!;
var origHeadFile = Path.Combine(gitDir, "rebase-merge", "orig-head");
var ontoFile = Path.Combine(gitDir, "rebase-merge", "onto");
var doneFile = Path.Combine(gitDir, "rebase-merge", "done");
var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json");
if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile))
var jobsFile = Path.Combine(Path.GetDirectoryName(file)!, "sourcegit_rebase_jobs.json");
if (!File.Exists(jobsFile))
return true;
var origHead = File.ReadAllText(origHeadFile).Trim();
var onto = File.ReadAllText(ontoFile).Trim();
var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection);
if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead))
var doneFile = Path.Combine(Path.GetDirectoryName(file)!, "rebase-merge", "done");
if (!File.Exists(doneFile))
return true;
var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (done.Length == 0)
var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
if (done.Length > collection.Jobs.Count)
return true;
var current = done[^1].Trim();
var match = REG_REBASE_TODO().Match(current);
if (!match.Success)
return true;
var sha = match.Groups[1].Value;
foreach (var job in collection.Jobs)
{
if (job.SHA.StartsWith(sha))
{
File.WriteAllText(file, job.Message);
break;
}
}
var job = collection.Jobs[done.Length - 1];
File.WriteAllText(file, job.Message);
exitCode = 0;
return true;
}
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{
var args = desktop.Args;
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
@ -526,18 +507,14 @@ namespace SourceGit
var file = args[1];
if (!File.Exists(file))
{
desktop.Shutdown(-1);
return true;
}
else
desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file);
var editor = new Views.StandaloneCommitMessageEditor();
editor.SetFile(file);
desktop.MainWindow = editor;
return true;
}
private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop)
{
var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS");
if (launchAsAskpass is not "TRUE")
@ -546,158 +523,30 @@ namespace SourceGit
var args = desktop.Args;
if (args?.Length > 0)
{
var askpass = new Views.Askpass();
askpass.TxtDescription.Text = args[0];
desktop.MainWindow = askpass;
desktop.MainWindow = new Views.Askpass(args[0]);
return true;
}
return false;
}
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{
Native.OS.SetupExternalTools();
Native.OS.SetupEnternalTools();
Models.AvatarManager.Instance.Start();
string startupRepo = null;
if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]))
startupRepo = desktop.Args[0];
var pref = ViewModels.Preferences.Instance;
pref.SetCanModify();
_launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
#if !DISABLE_UPDATE_DETECTION
var pref = ViewModels.Preference.Instance;
if (pref.ShouldCheck4UpdateOnStartup())
Check4Update();
#endif
}
private void TryOpenRepository(string repo)
{
if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo))
{
var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd();
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
{
Dispatcher.UIThread.Invoke(() =>
{
var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false);
ViewModels.Welcome.Instance.Refresh();
_launcher?.OpenRepositoryInTab(node, null);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd })
wnd.BringToTop();
});
return;
}
}
Dispatcher.UIThread.Invoke(() =>
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher })
launcher.BringToTop();
});
}
private void Check4Update(bool manually = false)
{
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(() =>
{
ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true);
});
}
private string FixFontFamilyName(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var parts = input.Split(',');
var trimmed = new List<string>();
foreach (var part in parts)
{
var t = part.Trim();
if (string.IsNullOrEmpty(t))
continue;
// Collapse multiple spaces into single space
var prevChar = '\0';
var sb = new StringBuilder();
foreach (var c in t)
{
if (c == ' ' && prevChar == ' ')
continue;
sb.Append(c);
prevChar = c;
}
var name = sb.ToString();
if (name.Contains('#', StringComparison.Ordinal))
{
if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) &&
!name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal))
continue;
}
trimmed.Add(name);
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
[GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")]
private static partial Regex REG_REBASE_TODO();
private Models.IpcChannel _ipcChannel = null;
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,17 +21,10 @@ namespace SourceGit.Commands
public Models.BlameData Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
return _result;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
var succ = Exec();
if (!succ)
{
ParseLine(line);
if (_result.IsBinary)
break;
return new Models.BlameData();
}
if (_needUnifyCommitSHA)
@ -49,9 +42,14 @@ namespace SourceGit.Commands
return _result;
}
private void ParseLine(string line)
protected override void OnReadline(string line)
{
if (line.Contains('\0', StringComparison.Ordinal))
if (_result.IsBinary)
return;
if (string.IsNullOrEmpty(line))
return;
if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
{
_result.IsBinary = true;
_result.LineInfos.Clear();
@ -67,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(_dateFormat);
var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd");
var info = new Models.BlameLineInfo()
{
@ -89,7 +87,6 @@ namespace SourceGit.Commands
private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder();
private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly;
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;

View file

@ -1,52 +1,30 @@
using System.Text;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public static class Branch
{
public static string ShowCurrent(string repo)
public static bool Create(string repo, string name, string basedOn)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch --show-current";
return cmd.ReadToEnd().StdOut.Trim();
}
public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log)
{
var builder = new StringBuilder();
builder.Append("branch ");
if (force)
builder.Append("-f ");
builder.Append(name);
builder.Append(" ");
builder.Append(basedOn);
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = builder.ToString();
cmd.Log = log;
cmd.Args = $"branch {name} {basedOn}";
return cmd.Exec();
}
public static bool Rename(string repo, string name, string to, Models.ICommandLog log)
public static bool Rename(string repo, string name, string to)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -M {name} {to}";
cmd.Log = log;
return cmd.Exec();
}
public static bool SetUpstream(string repo, string name, string upstream, Models.ICommandLog log)
public static bool SetUpstream(string repo, string name, string upstream)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Log = log;
if (string.IsNullOrEmpty(upstream))
cmd.Args = $"branch {name} --unset-upstream";
@ -56,27 +34,22 @@ namespace SourceGit.Commands
return cmd.Exec();
}
public static bool DeleteLocal(string repo, string name, Models.ICommandLog log)
public static bool DeleteLocal(string repo, string name)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -D {name}";
cmd.Log = log;
return cmd.Exec();
}
public static bool DeleteRemote(string repo, string remote, string name, Models.ICommandLog log)
public static bool DeleteRemote(string repo, string remote, string name)
{
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
return new Push(repo, remote, $"refs/heads/{name}", true) { Log = log }.Exec();
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -D -r {remote}/{name}";
cmd.Log = log;
cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
cmd.Args = $"push {remote} --delete {name}";
return cmd.Exec();
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,36 +4,19 @@ namespace SourceGit.Commands
{
public class Commit : Command
{
public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor)
public Commit(string repo, string message, bool amend, bool allowEmpty = false)
{
_tmpFile = Path.GetTempFileName();
File.WriteAllText(_tmpFile, message);
var file = Path.GetTempFileName();
File.WriteAllText(file, message);
WorkingDirectory = repo;
Context = repo;
Args = $"commit --allow-empty --file=\"{_tmpFile}\"";
if (signOff)
Args += " --signoff";
TraitErrorAsOutput = true;
Args = $"commit --file=\"{file}\"";
if (amend)
Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit";
Args += " --amend --no-edit";
if (allowEmpty)
Args += " --allow-empty";
}
public bool Run()
{
var succ = Exec();
try
{
File.Delete(_tmpFile);
}
catch
{
// Ignore
}
return succ;
}
private readonly string _tmpFile;
}
}

View file

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

View file

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

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain";
Args = "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(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
return lines.Length;
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@ -8,10 +8,6 @@ namespace SourceGit.Commands
{
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR();
[GeneratedRegex(@"^index\s([0-9a-f]{6,40})\.\.([0-9a-f]{6,40})(\s[1-9]{6})?")]
private static partial Regex REG_HASH_CHANGE();
private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/";
private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/";
private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/";
@ -28,48 +24,34 @@ namespace SourceGit.Commands
Context = repo;
if (ignoreWhitespace)
Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}";
else if (Models.DiffOption.IgnoreCRAtEOL)
Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
Args = $"diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
else
Args = $"diff --no-ext-diff --patch --unified={unified} {opt}";
Args = $"diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
}
public Models.DiffResult Result()
{
var rs = ReadToEnd();
var start = 0;
var end = rs.StdOut.IndexOf('\n', start);
while (end > 0)
{
var line = rs.StdOut.Substring(start, end - start);
ParseLine(line);
Exec();
start = end + 1;
end = rs.StdOut.IndexOf('\n', start);
}
if (start < rs.StdOut.Length)
ParseLine(rs.StdOut.Substring(start));
if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0)
if (_result.IsBinary || _result.IsLFS)
{
_result.TextDiff = null;
}
else
{
ProcessInlineHighlights();
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
if (_result.TextDiff.Lines.Count == 0)
_result.TextDiff = null;
else
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
return _result;
}
private void ParseLine(string line)
protected override void OnReadline(string line)
{
if (_result.IsBinary)
return;
if (line.StartsWith("old mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(9);
@ -82,17 +64,8 @@ namespace SourceGit.Commands
return;
}
if (line.StartsWith("deleted file mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(18);
if (_result.IsBinary)
return;
}
if (line.StartsWith("new file mode ", StringComparison.Ordinal))
{
_result.NewMode = line.Substring(14);
return;
}
if (_result.IsLFS)
{
@ -105,7 +78,7 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("-size ", StringComparison.Ordinal))
{
_result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
_result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
}
}
else if (ch == '+')
@ -116,52 +89,36 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = long.Parse(line.AsSpan(6));
_result.LFSDiff.New.Size = long.Parse(line.Substring(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
}
return;
}
if (_result.TextDiff.Lines.Count == 0)
{
if (line.StartsWith("Binary", StringComparison.Ordinal))
var match = REG_INDICATOR().Match(line);
if (!match.Success)
{
_result.IsBinary = true;
if (line.StartsWith("Binary", StringComparison.Ordinal))
_result.IsBinary = true;
return;
}
if (string.IsNullOrEmpty(_result.OldHash))
{
var match = REG_HASH_CHANGE().Match(line);
if (!match.Success)
return;
_result.OldHash = match.Groups[1].Value;
_result.NewHash = match.Groups[2].Value;
}
else
{
var match = REG_INDICATOR().Match(line);
if (!match.Success)
return;
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0);
_result.TextDiff.Lines.Add(_last);
}
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
}
else
{
if (line.Length == 0)
{
ProcessInlineHighlights();
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
_oldLine++;
_newLine++;
return;
@ -177,8 +134,7 @@ namespace SourceGit.Commands
return;
}
_last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0);
_deleted.Add(_last);
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
_oldLine++;
}
else if (ch == '+')
@ -190,8 +146,7 @@ namespace SourceGit.Commands
return;
}
_last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine);
_added.Add(_last);
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
_newLine++;
}
else if (ch != '\\')
@ -202,8 +157,7 @@ namespace SourceGit.Commands
{
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0);
_result.TextDiff.Lines.Add(_last);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
}
else
{
@ -214,16 +168,11 @@ namespace SourceGit.Commands
return;
}
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine));
_oldLine++;
_newLine++;
}
}
else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal))
{
_last.NoNewLineEndOfFile = true;
}
}
}
@ -274,7 +223,6 @@ namespace SourceGit.Commands
private readonly Models.DiffResult _result = new Models.DiffResult();
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private Models.TextDiffLine _last = null;
private int _oldLine = 0;
private int _newLine = 0;
}

View file

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

View file

@ -1,86 +0,0 @@
using System;
using System.Diagnostics;
using System.Text;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class ExecuteCustomAction
{
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, Models.ICommandLog log)
{
var start = new ProcessStartInfo();
start.FileName = file;
start.Arguments = args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo;
log?.AppendLine($"$ {file} {args}\n");
var proc = new Process() { StartInfo = start };
var builder = new StringBuilder();
proc.OutputDataReceived += (_, e) =>
{
if (e.Data != null)
log?.AppendLine(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (e.Data != null)
{
log?.AppendLine(e.Data);
builder.AppendLine(e.Data);
}
};
try
{
proc.Start();
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));
}
proc.Close();
}
}
}

View file

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

View file

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

View file

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

View file

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Text;
using System.Threading;
using Avalonia.Threading;
namespace SourceGit.Commands
{
/// <summary>
@ -22,78 +20,100 @@ namespace SourceGit.Commands
}
}
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
public GenerateCommitMessage(string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
{
_service = service;
_repo = repo;
_changes = changes;
_cancelToken = cancelToken;
_onResponse = onResponse;
_onProgress = onProgress;
}
public void Exec()
public string Result()
{
try
{
_onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
var responseBuilder = new StringBuilder();
var summaryBuilder = new StringBuilder();
var summaries = new List<string>();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
return;
return "";
responseBuilder.Append("- ");
summaryBuilder.Append("- ");
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);
_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");
_onProgress?.Invoke($"Analyzing {change.Path}...");
var summary = GenerateChangeSummary(change);
summaries.Add(summary);
}
if (_cancelToken.IsCancellationRequested)
return;
return "";
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}");
});
_onProgress?.Invoke($"Generating commit message...");
var builder = new StringBuilder();
builder.Append(GenerateSubject(string.Join("", summaries)));
builder.Append("\n");
foreach (var summary in summaries)
{
builder.Append("\n- ");
builder.Append(summary.Trim());
}
return builder.ToString();
}
catch (Exception e)
{
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
return "";
}
}
private Models.OpenAIService _service;
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 prompt = new StringBuilder();
prompt.AppendLine("You are an expert developer specialist in creating commits.");
prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:");
prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points.");
prompt.AppendLine("- Do not mention the route of file that has been change.");
prompt.AppendLine("- Simply describe the MAIN GOAL of the changes.");
prompt.AppendLine("- Output directly the summary in plain text.`");
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"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 prompt = new StringBuilder();
prompt.AppendLine("You are an expert developer specialist in creating commits messages.");
prompt.AppendLine("Your only goal is to retrieve a single commit message.");
prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:");
prompt.AppendLine("- Assign the commit {type} according to the next conditions:");
prompt.AppendLine(" feat: Only when adding a new feature.");
prompt.AppendLine(" fix: When fixing a bug.");
prompt.AppendLine(" docs: When updating documentation.");
prompt.AppendLine(" style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.");
prompt.AppendLine(" test: When adding or updating tests. ");
prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. ");
prompt.AppendLine(" revert: When undoing a previous commit.");
prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.");
prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer.");
prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.");
prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters.");
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private string _repo;
private List<Models.Change> _changes;
private CancellationToken _cancelToken;
private Action<string> _onResponse;
private Action<string> _onProgress;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,36 +1,23 @@
using System.Collections.Generic;
using System.Text;
using System;
namespace SourceGit.Commands
{
public class Merge : Command
{
public Merge(string repo, string source, string mode)
public Merge(string repo, string source, string mode, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
Args = $"merge --progress {source} {mode}";
}
public Merge(string repo, List<string> targets, bool autoCommit, string strategy)
protected override void OnReadline(string line)
{
WorkingDirectory = repo;
Context = repo;
var builder = new StringBuilder();
builder.Append("merge --progress ");
if (!string.IsNullOrEmpty(strategy))
builder.Append($"--strategy={strategy} ");
if (!autoCommit)
builder.Append("--no-commit ");
foreach (var t in targets)
{
builder.Append(t);
builder.Append(' ');
}
Args = builder.ToString();
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
}
}

View file

@ -13,18 +13,15 @@ namespace SourceGit.Commands
cmd.Context = repo;
cmd.RaiseError = true;
// NOTE: If no <file> names are specified, 'git mergetool' will run the merge tool program on every file with merge conflicts.
var fileArg = string.IsNullOrEmpty(file) ? "" : $"\"{file}\"";
if (toolType == 0)
{
cmd.Args = $"mergetool {fileArg}";
cmd.Args = $"mergetool \"{file}\"";
return cmd.Exec();
}
if (!File.Exists(toolPath))
{
Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!"));
Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!"));
return false;
}
@ -35,7 +32,7 @@ namespace SourceGit.Commands
return false;
}
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {fileArg}";
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\"";
return cmd.Exec();
}
@ -54,7 +51,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath))
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!"));
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!"));
return false;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryCommitChildren : Command
{
public QueryCommitChildren(string repo, string commit, int max)
{
WorkingDirectory = repo;
Context = repo;
_commit = commit;
Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}";
}
public List<string> Result()
{
var rs = ReadToEnd();
var outs = new List<string>();
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains(_commit))
outs.Add(line.Substring(0, 40));
}
}
return outs;
}
private string _commit;
}
}

View file

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

View file

@ -1,34 +0,0 @@
namespace SourceGit.Commands
{
public class QueryCommitSignInfo : Command
{
public QueryCommitSignInfo(string repo, string sha, bool useFakeSignersFile)
{
WorkingDirectory = repo;
Context = repo;
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}";
}
public Models.CommitSignInfo Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
return null;
var raw = rs.StdOut.Trim().ReplaceLineEndings("\n");
if (raw.Length <= 1)
return null;
var lines = raw.Split('\n');
return new Models.CommitSignInfo()
{
VerifyResult = lines[0][0],
Signer = lines[1],
Key = lines[2]
};
}
}
}

View file

@ -10,7 +10,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"log --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
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%s " + limits;
_findFirstMerged = needFindHead;
}
@ -18,20 +18,20 @@ namespace SourceGit.Commands
{
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
if (method == Models.CommitSearchMethod.ByAuthor)
if (method == Models.CommitSearchMethod.ByUser)
{
search += $"-i --author=\"{filter}\"";
search += $"-i --author=\"{filter}\" --committer=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByCommitter)
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-i --committer=\"{filter}\"";
search += $"-- \"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByMessage)
else
{
var argsBuilder = new StringBuilder();
argsBuilder.Append(search);
var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries);
var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
@ -41,18 +41,10 @@ namespace SourceGit.Commands
search = argsBuilder.ToString();
}
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
{
search = $"-G\"{filter}\"";
}
WorkingDirectory = repo;
Context = repo;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {search}";
Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
_findFirstMerged = false;
}
@ -120,7 +112,15 @@ namespace SourceGit.Commands
if (data.Length < 8)
return;
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
var idx = data.IndexOf(' ', StringComparison.Ordinal);
if (idx == -1)
{
_current.Parents.Add(data);
return;
}
_current.Parents.Add(data.Substring(0, idx));
_current.Parents.Add(data.Substring(idx + 1));
}
private void MarkFirstMerged()
@ -128,7 +128,7 @@ namespace SourceGit.Commands
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0)
return;

View file

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

View file

@ -0,0 +1,21 @@
namespace SourceGit.Commands
{
public class QueryCurrentRevisionFiles : Command
{
public QueryCurrentRevisionFiles(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "ls-tree -r --name-only HEAD";
}
public string[] Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
return rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
return [];
}
}
}

View file

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

View file

@ -11,11 +11,14 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree {revision} -l -- \"{file}\"";
Args = $"ls-tree {revision} -l -- {file}";
}
public long Result()
{
if (_result != 0)
return _result;
var rs = ReadToEnd();
if (rs.IsSuccess)
{
@ -26,5 +29,7 @@ namespace SourceGit.Commands
return 0;
}
private readonly long _result = 0;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree -z {sha}";
Args = $"ls-tree {sha}";
if (!string.IsNullOrEmpty(parentFolder))
Args += $" -- \"{parentFolder}\"";
@ -20,27 +20,11 @@ namespace SourceGit.Commands
public List<Models.Object> Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
{
var start = 0;
var end = rs.StdOut.IndexOf('\0', start);
while (end > 0)
{
var line = rs.StdOut.Substring(start, end - start);
Parse(line);
start = end + 1;
end = rs.StdOut.IndexOf('\0', start);
}
if (start < rs.StdOut.Length)
Parse(rs.StdOut.Substring(start));
}
Exec();
return _objects;
}
private void Parse(string line)
protected override void OnReadline(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)

View file

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

View file

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

View file

@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryStashChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
public QueryStashChanges(string repo, string sha)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
}
public List<Models.Change> Result()
{
Exec();
return _changes;
}
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])
{
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;
}
}
private readonly List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
namespace SourceGit.Commands
{
@ -9,65 +8,41 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s";
Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s";
}
public List<Models.Stash> Result()
{
var outs = new List<Models.Stash>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return outs;
Exec();
return _stashes;
}
var nextPartIdx = 0;
var start = 0;
var end = rs.StdOut.IndexOf('\n', start);
while (end > 0)
protected override void OnReadline(string line)
{
switch (_nextLineIdx)
{
var line = rs.StdOut.Substring(start, end - start);
switch (nextPartIdx)
{
case 0:
_current = new Models.Stash() { SHA = line };
outs.Add(_current);
break;
case 1:
ParseParent(line);
break;
case 2:
_current.Time = ulong.Parse(line);
break;
case 3:
_current.Name = line;
break;
case 4:
_current.Message = line;
break;
}
nextPartIdx++;
if (nextPartIdx > 4)
nextPartIdx = 0;
start = end + 1;
end = rs.StdOut.IndexOf('\n', start);
case 0:
_current = new Models.Stash() { SHA = line };
_stashes.Add(_current);
break;
case 1:
_current.Time = ulong.Parse(line);
break;
case 2:
_current.Name = line;
break;
case 3:
_current.Message = line;
break;
}
if (start < rs.StdOut.Length)
_current.Message = rs.StdOut.Substring(start);
return outs;
}
private void ParseParent(string data)
{
if (data.Length < 8)
return;
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
_nextLineIdx++;
if (_nextLineIdx > 3)
_nextLineIdx = 0;
}
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
private int _nextLineIdx = 0;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -45,14 +45,5 @@
Args = "remote set-url" + (isPush ? " --push " : " ") + $"{name} {url}";
return Exec();
}
public bool HasBranch(string remote, string branch)
{
SSHKey = new Config(WorkingDirectory).Get($"remote.{remote}.sshkey");
Args = $"ls-remote {remote} {branch}";
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Trim().Length > 0;
}
}
}

View file

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

View file

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

View file

@ -9,7 +9,7 @@ namespace SourceGit.Commands
{
public static class SaveChangesAsPatch
{
public static bool ProcessLocalChanges(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo)
public static bool Exec(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo)
{
using (var sw = File.Create(saveTo))
{
@ -23,33 +23,6 @@ namespace SourceGit.Commands
return true;
}
public static bool ProcessRevisionCompareChanges(string repo, List<Models.Change> changes, string baseRevision, string targetRevision, string saveTo)
{
using (var sw = File.Create(saveTo))
{
foreach (var change in changes)
{
if (!ProcessSingleChange(repo, new Models.DiffOption(baseRevision, targetRevision, change), sw))
return false;
}
}
return true;
}
public static bool ProcessStashChanges(string repo, List<Models.DiffOption> opts, string saveTo)
{
using (var sw = File.Create(saveTo))
{
foreach (var opt in opts)
{
if (!ProcessSingleChange(repo, opt, sw))
return false;
}
}
return true;
}
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
{
var starter = new ProcessStartInfo();

View file

@ -13,8 +13,12 @@ namespace SourceGit.Commands
var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result();
if (isLFSFiltered)
{
var pointerStream = QueryFileContent.Run(repo, revision, file);
ExecCmd(repo, $"lfs smudge", saveTo, pointerStream);
var tmpFile = saveTo + ".tmp";
if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile))
{
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
}
File.Delete(tmpFile);
}
else
{
@ -22,7 +26,7 @@ namespace SourceGit.Commands
}
}
private static bool ExecCmd(string repo, string args, string outputFile, Stream input = null)
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
@ -41,8 +45,21 @@ namespace SourceGit.Commands
{
var proc = new Process() { StartInfo = starter };
proc.Start();
if (input != null)
proc.StandardInput.Write(new StreamReader(input).ReadToEnd());
if (inputFile != null)
{
using (StreamReader sr = new StreamReader(inputFile))
{
while (true)
{
var line = sr.ReadLine();
if (line == null)
break;
proc.StandardInput.WriteLine(line);
}
}
}
proc.StandardOutput.BaseStream.CopyTo(sw);
proc.WaitForExit();
var rs = proc.ExitCode == 0;

View file

@ -11,84 +11,69 @@ namespace SourceGit.Commands
Context = repo;
}
public bool Push(string message, bool includeUntracked = true, bool keepIndex = false)
public bool Push(string message)
{
var builder = new StringBuilder();
builder.Append("stash push ");
if (includeUntracked)
builder.Append("--include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\"");
Args = builder.ToString();
Args = $"stash push -m \"{message}\"";
return Exec();
}
public bool Push(string message, List<Models.Change> changes, bool keepIndex)
public bool Push(List<Models.Change> changes, string message, bool onlyStaged)
{
var builder = new StringBuilder();
builder.Append("stash push --include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\" -- ");
var pathsBuilder = new StringBuilder();
foreach (var c in changes)
builder.Append($"\"{c.Path}\" ");
if (onlyStaged)
{
foreach (var c in changes)
pathsBuilder.Append($"\"{c.Path}\" ");
var paths = pathsBuilder.ToString();
Args = $"stash push --staged -m \"{message}\" -- {paths}";
}
else
{
var needAdd = new List<Models.Change>();
foreach (var c in changes)
{
pathsBuilder.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();
}
var paths = pathsBuilder.ToString();
Args = $"stash push -m \"{message}\" -- {paths}";
}
Args = builder.ToString();
return Exec();
}
public bool Push(string message, string pathspecFromFile, bool keepIndex)
public bool Apply(string 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}\"";
Args = $"stash apply -q {name}";
return Exec();
}
public bool Pop(string name)
{
Args = $"stash pop -q --index \"{name}\"";
Args = $"stash pop -q {name}";
return Exec();
}
public bool Drop(string name)
{
Args = $"stash drop -q \"{name}\"";
Args = $"stash drop -q {name}";
return Exec();
}

View file

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

View file

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

View file

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

View file

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

19
src/Commands/Version.cs Normal file
View file

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

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands
{
@ -21,13 +20,12 @@ namespace SourceGit.Commands
var last = null as Models.Worktree;
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
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))
@ -56,7 +54,7 @@ namespace SourceGit.Commands
return worktrees;
}
public bool Add(string fullpath, string name, bool createNew, string tracking)
public bool Add(string fullpath, string name, bool createNew, string tracking, Action<string> outputHandler)
{
Args = "worktree add ";
@ -75,15 +73,15 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(tracking))
Args += tracking;
else if (!string.IsNullOrEmpty(name) && !createNew)
Args += name;
_outputHandler = outputHandler;
return Exec();
}
public bool Prune()
public bool Prune(Action<string> outputHandler)
{
Args = "worktree prune -v";
_outputHandler = outputHandler;
return Exec();
}
@ -99,14 +97,22 @@ namespace SourceGit.Commands
return Exec();
}
public bool Remove(string fullpath, bool force)
public bool Remove(string fullpath, bool force, Action<string> outputHandler)
{
if (force)
Args = $"worktree remove -f \"{fullpath}\"";
else
Args = $"worktree remove \"{fullpath}\"";
_outputHandler = outputHandler;
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler = null;
}
}

View file

@ -1,5 +1,4 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace SourceGit.Converters
{
@ -7,8 +6,5 @@ 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);
}
}

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