Compare commits

..

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

521 changed files with 14876 additions and 44465 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

70
.gitattributes vendored
View file

@ -1,14 +1,78 @@
# Auto detect text files and perform LF normalization
# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/
* text=auto
#
# The above will handle all files NOT found below
#
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text
*.tab text
*.tsv text
*.txt text
*.sql text
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as an asset (binary) by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf
*.spec text eol=lf
control text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text
# Archives
*.7z binary
*.gz binary
*.tar binary
*.tgz binary
*.zip binary
# Text files where line endings should be preserved
*.patch -text
#
# Exclude files from exporting
#
.gitattributes export-ignore
.gitignore export-ignore
.gitignore export-ignore

View file

@ -1,79 +0,0 @@
name: Build
on:
workflow_call:
jobs:
build:
strategy:
matrix:
include:
- name : Windows x64
os: windows-2019
runtime: win-x64
- name : Windows ARM64
os: windows-2019
runtime: win-arm64
- name : macOS (Intel)
os: macos-13
runtime: osx-x64
- name : macOS (Apple Silicon)
os: macos-latest
runtime: osx-arm64
- name : Linux
os: ubuntu-latest
runtime: linux-x64
container: ubuntu:20.04
- name : Linux (arm64)
os: ubuntu-latest
runtime: linux-arm64
container: ubuntu:20.04
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
container: ${{ matrix.container || '' }}
steps:
- name: Install common CLI tools
if: ${{ startsWith(matrix.runtime, 'linux-') }}
run: |
export DEBIAN_FRONTEND=noninteractive
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
apt-get update
apt-get install -y sudo
sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Configure arm64 packages
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
sudo dpkg --add-architecture arm64
echo 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted' \
| sudo tee /etc/apt/sources.list.d/arm64.list
sudo sed -i -e 's/^deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
sudo sed -i -e 's/^deb mirror/deb [arch=amd64] mirror/g' /etc/apt/sources.list
- name: Install cross-compiling dependencies
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
sudo apt-get update
sudo apt-get install -y llvm gcc-aarch64-linux-gnu
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r ${{ matrix.runtime }}
- name: Rename executable file
if: ${{ startsWith(matrix.runtime, 'linux-') }}
run: mv publish/SourceGit publish/sourcegit
- name: Tar artifact
if: ${{ startsWith(matrix.runtime, 'linux-') || startsWith(matrix.runtime, 'osx-') }}
run: |
tar -cvf "sourcegit.${{ matrix.runtime }}.tar" -C publish .
rm -r publish/*
mv "sourcegit.${{ matrix.runtime }}.tar" publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: publish/*

View file

@ -1,29 +1,139 @@
name: Continuous Integration
on:
push:
branches: [develop]
branches:
- develop
pull_request:
branches: [develop]
workflow_dispatch:
workflow_call:
jobs:
build:
name: Build
uses: ./.github/workflows/build.yml
version:
name: Prepare version string
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
build-windows:
name: Build Windows x64
runs-on: windows-2019
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Output version string
id: version
run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"
package:
needs: [build, version]
name: Package
uses: ./.github/workflows/package.yml
with:
version: ${{ needs.version.outputs.version }}
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r win-x64
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.win-x64
path: publish
build-macos-intel:
name: Build macOS (Intel)
runs-on: macos-13
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r osx-x64
- name: Packing Program
run: tar -cvf sourcegit.osx-x64.tar -C publish/ .
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.osx-x64
path: sourcegit.osx-x64.tar
build-macos-arm64:
name: Build macOS (Apple Silicon)
runs-on: macos-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r osx-arm64
- name: Packing Program
run: tar -cvf sourcegit.osx-arm64.tar -C publish/ .
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.osx-arm64
path: sourcegit.osx-arm64.tar
build-linux:
name: Build Linux
runs-on: ubuntu-20.04
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r linux-x64
- name: Rename Executable File
run: mv publish/SourceGit publish/sourcegit
- name: Packing Program
run: tar -cvf sourcegit.linux-x64.tar -C publish/ .
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.linux-x64
path: sourcegit.linux-x64.tar
build-linux-arm64:
name: Build Linux (arm64)
runs-on: ubuntu-20.04
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Configure arm64 packages
run: |
sudo dpkg --add-architecture arm64
echo 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted' \
| sudo tee /etc/apt/sources.list.d/arm64.list
sudo sed -i -e 's/^deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
sudo sed -i -e 's/^deb mirror/deb [arch=amd64] mirror/g' /etc/apt/sources.list
- name: Install cross-compiling dependencies
run: |
sudo apt-get update
sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r linux-arm64
- name: Rename Executable File
run: mv publish/SourceGit publish/sourcegit
- name: Packing Program
run: tar -cvf sourcegit.linux-arm64.tar -C publish/ .
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.linux-arm64
path: sourcegit.linux-arm64.tar

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

@ -1,111 +0,0 @@
name: Package
on:
workflow_call:
inputs:
version:
description: SourceGit package version
required: true
type: string
jobs:
windows:
name: Package Windows
runs-on: windows-2019
strategy:
matrix:
runtime: [ win-x64, win-arm64 ]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: build/SourceGit
- name: Package
shell: bash
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
run: ./build/scripts/package.windows.sh
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: build/sourcegit_*.zip
- name: Delete temp artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: sourcegit.${{ matrix.runtime }}
osx-app:
name: Package macOS
runs-on: macos-latest
strategy:
matrix:
runtime: [osx-x64, osx-arm64]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: build
- name: Package
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
run: |
mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
./build/scripts/package.osx-app.sh
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: build/sourcegit_*.zip
- name: Delete temp artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: sourcegit.${{ matrix.runtime }}
linux:
name: Package Linux
runs-on: ubuntu-latest
container: ubuntu:20.04
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
steps:
- name: Checkout sources
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
- name: Download build
uses: actions/download-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: build
- name: Package
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
./build/scripts/package.linux.sh
- name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: |
build/sourcegit-*.AppImage
build/sourcegit_*.deb
build/sourcegit-*.rpm
- name: Delete temp artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: sourcegit.${{ matrix.runtime }}

View file

@ -1,52 +0,0 @@
name: Release
on:
push:
tags:
- v*
jobs:
build:
name: Build
uses: ./.github/workflows/build.yml
version:
name: Prepare version string
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Output version string
id: version
env:
TAG: ${{ github.ref_name }}
run: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
package:
needs: [build, version]
name: Package
uses: ./.github/workflows/package.yml
with:
version: ${{ needs.version.outputs.version }}
release:
needs: [package, version]
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.version.outputs.version }}
run: gh release create "$TAG" -t "$VERSION" --notes-from-tag
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: package.*
path: packages
merge-multiple: true
- name: Upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" packages/*

596
.gitignore vendored
View file

@ -1,13 +1,425 @@
.vs/
.vscode/
.idea/
*.sln.docstates
*.user
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Nuke Build - Uncomment if you are using it
.nuke/temp
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### Linux ###
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
@ -16,18 +428,175 @@
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Rider ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### VisualStudioCode ###
!.vscode/*.code-snippets
# Local History for Visual Studio Code
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
bin/
obj/
# ignore ci node files
node_modules/
package.json
package-lock.json
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
# Windows shortcuts
*.lnk
### Specifics ###
# Specials
*.zip
archives
package-lock.json
*.private.env.json
**/**/[Dd]ata/*.json
**/**/[Dd]ata/*.csv
# SpecFlow
*.feature.cs
# Azurite
*azurite*.json
# Build Folders
[Pp]ublish
[Oo]utput
[Ss]cripts
[Tt]ests/[Rr]esults
# LibraryManager
**/lib
# BuildBundlerMinifier
*.min.*
*.map
# Sass Output
**/css
# SQLite files
*.db
*.sqlite3
*.sqlite
*.db-journal
*.sqlite3-journal
*.sqlite-journal
*.db-shm
*.db-wal
# SourceGit output files
build/resources/
build/SourceGit/
build/SourceGit.app/
@ -36,6 +605,3 @@ build/*.tar.gz
build/*.deb
build/*.rpm
build/*.AppImage
SourceGit.app/
build.command
src/Properties/launchSettings.json

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.

160
README.md
View file

@ -1,26 +1,21 @@
# 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/German/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...
* Interactive rebase (Basic)
* Branches
* Remotes
* Tags
@ -35,40 +30,24 @@
* 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))
* GitFlow support
* Git LFS support
> [!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` |
| 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.
| OS | PATH |
|---------|-------------------------------------------------|
| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` |
| Linux | `${HOME}/.config/SourceGit` |
| macOS | `${HOME}/Library/Application Support/SourceGit` |
For **Windows** users:
@ -77,99 +56,43 @@ For **Windows** users:
```shell
winget install SourceGit
```
> [!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:
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
* You can install the latest stable by `scoope` with follow commands:
```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.
For `OpenAI`:
* `Server` must be `https://api.openai.com/v1`
For other AI service:
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate`
* The `API Key` is optional that depends on the service
## External Tools
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 | Environment Variable |
|-------------------------------|---------|-------|-------|----------------------|
| Visual Studio Code | YES | YES | YES | VSCODE_PATH |
| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS_PATH |
| VSCodium | YES | YES | YES | VSCODIUM_PATH |
| JetBrains Fleet | YES | YES | YES | FLEET_PATH |
| Sublime Text | YES | YES | YES | SUBLIME_TEXT_PATH |
> [!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:
```json
{
"tools": {
"Visual Studio Code": "D:\\VSCode\\Code.exe"
}
}
```
> [!NOTE]
> This app also supports a lot of `JetBrains` IDEs, installing `JetBrains Toolbox` will help this app to find them.
* You can set the given environment variable for special tool if it can NOT be found by this app automatically.
* Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device.
* On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables.
## Screenshots
@ -183,25 +106,12 @@ This app supports open repository in external tools listed in the table below.
* Custom
You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git). And welcome to share your own themes.
You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git)
## Contributing
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

@ -6,6 +6,11 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGit", "src\SourceGit.csproj", "{2091C34D-4A17-4375-BEF3-4D60BE8113E4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{773082AC-D9C8-4186-8521-4B6A7BEE6158}"
ProjectSection(SolutionItems) = preProject
build\build.linux.sh = build\build.linux.sh
build\build.osx.command = build\build.osx.command
build\build.windows.ps1 = build\build.windows.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{FD384607-ED99-47B7-AF31-FB245841BC92}"
EndProject
@ -13,11 +18,7 @@ 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}"
@ -43,6 +44,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_common", "_common", "{04FD
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "usr", "usr", "{76639799-54BC-45E8-BD90-F45F63ACD11D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bin", "bin", "{2E27E952-846B-4D75-A426-D22151277864}"
ProjectSection(SolutionItems) = preProject
build\resources\_common\usr\bin\sourcegit = build\resources\_common\usr\bin\sourcegit
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "share", "share", "{A3ABAA7C-EE14-4448-B466-6E69C1347E7D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "applications", "applications", "{2AF28D3B-14A8-46A8-B828-157FAAB1B06F}"
@ -60,8 +66,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}"
@ -73,18 +77,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SPECS", "SPECS", "{7802CD7A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "appimage", "appimage", "{5D125DD9-B48A-491F-B2FB-D7830D74C4DC}"
ProjectSection(SolutionItems) = preProject
build\resources\appimage\publish-appimage = build\resources\appimage\publish-appimage
build\resources\appimage\publish-appimage.conf = build\resources\appimage\publish-appimage.conf
build\resources\appimage\runtime-x86_64 = build\resources\appimage\runtime-x86_64
build\resources\appimage\sourcegit.appdata.xml = build\resources\appimage\sourcegit.appdata.xml
build\resources\appimage\sourcegit.png = build\resources\appimage\sourcegit.png
EndProjectSection
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
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -106,6 +105,7 @@ Global
{ABC98884-F023-4EF4-A9C9-5DE9452BE955} = {FD384607-ED99-47B7-AF31-FB245841BC92}
{04FD74B1-FBDB-496E-A48F-3D59D71FF952} = {FD384607-ED99-47B7-AF31-FB245841BC92}
{76639799-54BC-45E8-BD90-F45F63ACD11D} = {04FD74B1-FBDB-496E-A48F-3D59D71FF952}
{2E27E952-846B-4D75-A426-D22151277864} = {76639799-54BC-45E8-BD90-F45F63ACD11D}
{A3ABAA7C-EE14-4448-B466-6E69C1347E7D} = {76639799-54BC-45E8-BD90-F45F63ACD11D}
{2AF28D3B-14A8-46A8-B828-157FAAB1B06F} = {A3ABAA7C-EE14-4448-B466-6E69C1347E7D}
{7166EC6C-17F5-4B5E-B38E-1E53C81EACF6} = {A3ABAA7C-EE14-4448-B466-6E69C1347E7D}
@ -114,7 +114,6 @@ Global
{9BA0B044-0CC9-46F8-B551-204F149BF45D} = {FD384607-ED99-47B7-AF31-FB245841BC92}
{7802CD7A-591B-4EDD-96F8-9BF3F61692E4} = {9BA0B044-0CC9-46F8-B551-204F149BF45D}
{5D125DD9-B48A-491F-B2FB-D7830D74C4DC} = {FD384607-ED99-47B7-AF31-FB245841BC92}
{C54D4001-9940-477C-A0B6-E795ED0A3209} = {773082AC-D9C8-4186-8521-4B6A7BEE6158}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7FF1B9C6-B5BF-4A50-949F-4B407A0E31C9}

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.24

View file

@ -1,15 +0,0 @@
# build
> [!WARNING]
> The files under the `build` folder is used for `Github Action` only, **NOT** for end users.
## How to build this project manually
1. Make sure [.NET SDK 9](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.

32
build/build.linux.sh Executable file
View file

@ -0,0 +1,32 @@
#!/bin/sh
version=`cat ../VERSION`
# Cleanup
rm -rf SourceGit *.tar.gz resources/deb/opt *.deb *.rpm *.AppImage
# Generic AppImage
cd resources/appimage
./publish-appimage -y -o sourcegit-${version}.linux.x86_64.AppImage
# Move to build dir
mv AppImages/sourcegit-${version}.linux.x86_64.AppImage ../../
mv AppImages/AppDir/usr/bin ../../SourceGit
cd ../../
# Debain/Ubuntu package
mkdir -p resources/deb/opt/sourcegit/
mkdir -p resources/deb/usr/share/applications
mkdir -p resources/deb/usr/share/icons
cp -f SourceGit/* resources/deb/opt/sourcegit/
cp -r resources/_common/applications resources/deb/usr/share/
cp -r resources/_common/icons resources/deb/usr/share/
chmod +x -R resources/deb/opt/sourcegit
sed -i "2s/.*/Version: ${version}/g" resources/deb/DEBIAN/control
dpkg-deb --build resources/deb ./sourcegit_${version}-1_amd64.deb
# Redhat/CentOS/Fedora package
rpmbuild -bb --target=x86_64 resources/rpm/SPECS/build.spec --define "_topdir `pwd`/resources/rpm" --define "_version ${version}"
mv resources/rpm/RPMS/x86_64/sourcegit-${version}-1.x86_64.rpm .
rm -rf SourceGit

21
build/build.osx.command Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
version=`cat ../VERSION`
rm -rf SourceGit.app *.zip
mkdir -p SourceGit.app/Contents/Resources
cp resources/app/App.icns SourceGit.app/Contents/Resources/App.icns
sed "s/SOURCE_GIT_VERSION/${version}/g" resources/app/App.plist > SourceGit.app/Contents/Info.plist
mkdir -p SourceGit.app/Contents/MacOS
dotnet publish ../src/SourceGit.csproj -c Release -r osx-arm64 -o SourceGit.app/Contents/MacOS
zip sourcegit_${version}.osx-arm64.zip -r SourceGit.app -x "*/*\.dsym/*"
rm -rf SourceGit.app/Contents/MacOS
mkdir -p SourceGit.app/Contents/MacOS
dotnet publish ../src/SourceGit.csproj -c Release -r osx-x64 -o SourceGit.app/Contents/MacOS
zip sourcegit_${version}.osx-x64.zip -r SourceGit.app -x "*/*\.dsym/*"
rm -rf SourceGit.app

23
build/build.windows.ps1 Normal file
View file

@ -0,0 +1,23 @@
$version = Get-Content ..\VERSION
if (Test-Path SourceGit) {
Remove-Item SourceGit -Recurse -Force
}
Remove-Item *.zip -Force
dotnet publish ..\src\SourceGit.csproj -c Release -r win-arm64 -o SourceGit
Remove-Item SourceGit\*.pdb -Force
Compress-Archive -Path SourceGit -DestinationPath "sourcegit_$version.win-arm64.zip"
if (Test-Path SourceGit) {
Remove-Item SourceGit -Recurse -Force
}
dotnet publish ..\src\SourceGit.csproj -c Release -r win-x64 -o SourceGit
Remove-Item SourceGit\*.pdb -Force
Compress-Archive -Path SourceGit -DestinationPath "sourcegit_$version.win-x64.zip"

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

@ -12,6 +12,11 @@
<string>SOURCE_GIT_VERSION.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>LSEnvironment</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>CFBundleExecutable</key>
<string>SourceGit</string>
<key>CFBundleInfoDictionaryVersion</key>

View file

@ -0,0 +1,708 @@
#!/bin/bash
################################################################################
# PROJECT : Publish-AppImage for .NET
# WEBPAGE : https://github.com/kuiperzone/Publish-AppImage
# COPYRIGHT : Andy Thomas 2021-2023
# LICENSE : MIT
################################################################################
###############################
# CONSTANTS
###############################
declare -r _SCRIPT_VERSION="1.3.1"
declare -r _SCRIPT_TITLE="Publish-AppImage for .NET"
declare -r _SCRIPT_IMPL_MIN=1
declare -r _SCRIPT_IMPL_MAX=1
declare -r _SCRIPT_COPYRIGHT="Copyright 2023 Andy Thomas"
declare -r _SCRIPT_WEBSITE="https://github.com/kuiperzone/Publish-AppImage"
declare -r _SCRIPT_NAME="publish-appimage"
declare -r _DEFAULT_CONF="${_SCRIPT_NAME}.conf"
declare -r _APPIMAGE_KIND="appimage"
declare -r _ZIP_KIND="zip"
declare -r _DOTNET_NONE="null"
###############################
# FUNCTIONS
###############################
function assert_result
{
local _ret=$?
if [ ${_ret} -ne 0 ]; then
echo
exit ${_ret}
fi
}
function exec_or_die
{
echo "${1}"
eval "${1}"
assert_result
}
function ensure_directory
{
local _path="${1}"
if [ ! -d "${_path}" ]; then
mkdir -p "${_path}"
assert_result
fi
}
function remove_path
{
local _path="${1}"
if [ -d "${_path}" ]; then
rm -rf "${_path}"
assert_result
elif [ -f "${_path}" ]; then
rm -f "${_path}"
assert_result
fi
}
function assert_mandatory
{
local _name="${1}"
local _value="${2}"
if [ "${_value}" == "" ]; then
echo "${_name} undefined in: ${_conf_arg_value}"
echo
exit 1
fi
}
function assert_opt_file
{
local _name="${1}"
local _value="${2}"
if [ "${_value}" != "" ] && [ ! -f "${_value}" ]; then
echo "File not found: ${_value}"
if [ "${_name}" != "" ]; then
echo "See ${_name} in: ${_conf_arg_value}"
fi
echo
exit 1
fi
}
###############################
# HANDLE ARGUMENTS
###############################
# Specify conf file
declare -r _CONF_ARG="f"
declare -r _CONF_ARG_NAME="conf"
_conf_arg_value="${_DEFAULT_CONF}"
_arg_syntax=":${_CONF_ARG}:"
# Runtime ID
declare -r _RID_ARG="r"
declare -r _RID_ARG_NAME="runtime"
_rid_arg_value="linux-x64"
_arg_syntax="${_arg_syntax}${_RID_ARG}:"
# Package kind
declare -r _KIND_ARG="k"
declare -r _KIND_ARG_NAME="kind"
declare -l _kind_arg_value="${_APPIMAGE_KIND}"
_arg_syntax="${_arg_syntax}${_KIND_ARG}:"
# Run app
declare -r _RUNAPP_ARG="u"
declare -r _RUNAPP_ARG_NAME="run"
_runapp_arg_value=false
_arg_syntax="${_arg_syntax}${_RUNAPP_ARG}"
# Verbose
declare -r _VERBOSE_ARG="b"
declare -r _VERBOSE_ARG_NAME="verbose"
_verbose_arg_value=false
_arg_syntax="${_arg_syntax}${_VERBOSE_ARG}"
# Skip yes (no prompt)
declare -r _SKIPYES_ARG="y"
declare -r _SKIPYES_ARG_NAME="skip-yes"
_skipyes_arg_value=false
_arg_syntax="${_arg_syntax}${_SKIPYES_ARG}"
# Output name
declare -r _OUTPUT_ARG="o"
declare -r _OUTPUT_ARG_NAME="output"
_output_arg_value=""
_arg_syntax="${_arg_syntax}${_OUTPUT_ARG}:"
# Show version
declare -r _VERSION_ARG="v"
declare -r _VERSION_ARG_NAME="version"
_version_arg_value=false
_arg_syntax="${_arg_syntax}${_VERSION_ARG}"
# Show help
declare -r _HELP_ARG="h"
declare -r _HELP_ARG_NAME="help"
_help_arg_value=false
_arg_syntax="${_arg_syntax}${_HELP_ARG}"
_exit_help=0
# Transform long options to short ones
for arg in "${@}"; do
shift
case "${arg}" in
("--${_CONF_ARG_NAME}") set -- "$@" "-${_CONF_ARG}" ;;
("--${_RID_ARG_NAME}") set -- "$@" "-${_RID_ARG}" ;;
("--${_KIND_ARG_NAME}") set -- "$@" "-${_KIND_ARG}" ;;
("--${_RUNAPP_NAME}") set -- "$@" "-${_RUNAPP_ARG}" ;;
("--${_VERBOSE_ARG_NAME}") set -- "$@" "-${_VERBOSE_ARG}" ;;
("--${_SKIPYES_ARG_NAME}") set -- "$@" "-${_SKIPYES_ARG}" ;;
("--${_OUTPUT_ARG_NAME}") set -- "$@" "-${_OUTPUT_ARG}" ;;
("--${_VERSION_ARG_NAME}") set -- "$@" "-${_VERSION_ARG}" ;;
("--${_HELP_ARG_NAME}") set -- "$@" "-${_HELP_ARG}" ;;
("--"*)
echo "Illegal argument: ${arg}"
echo
_exit_help=1
break
;;
(*) set -- "$@" "${arg}" ;;
esac
done
if [ ${_exit_help} == 0 ]; then
# Read arguments
while getopts ${_arg_syntax} arg; do
case "${arg}" in
(${_CONF_ARG}) _conf_arg_value="${OPTARG}" ;;
(${_RID_ARG}) _rid_arg_value="${OPTARG}" ;;
(${_KIND_ARG}) _kind_arg_value="${OPTARG}" ;;
(${_RUNAPP_ARG}) _runapp_arg_value=true ;;
(${_VERBOSE_ARG}) _verbose_arg_value=true ;;
(${_SKIPYES_ARG}) _skipyes_arg_value=true ;;
(${_OUTPUT_ARG}) _output_arg_value="${OPTARG}" ;;
(${_VERSION_ARG}) _version_arg_value=true ;;
(${_HELP_ARG}) _help_arg_value=true ;;
(*)
echo "Illegal argument"
echo
_exit_help=1
break
;;
esac
done
fi
# Handle and help and version
if [ ${_help_arg_value} == true ] || [ $_exit_help != 0 ]; then
_indent=" "
echo "Usage:"
echo "${_indent}${_SCRIPT_NAME} [-flags] [-option-n value-n]"
echo
echo "Help Options:"
echo "${_indent}-${_HELP_ARG}, --${_HELP_ARG_NAME}"
echo "${_indent}Show help information flag."
echo
echo "${_indent}-${_VERSION_ARG}, --${_VERSION_ARG_NAME}"
echo "${_indent}Show version and about information flag."
echo
echo "Build Options:"
echo "${_indent}-${_CONF_ARG}, --${_CONF_ARG_NAME} value"
echo "${_indent}Specifies the conf file. Defaults to ${_SCRIPT_NAME}.conf."
echo
echo "${_indent}-${_RID_ARG}, --${_RID_ARG_NAME} value"
echo "${_indent}Dotnet publish runtime identifier. Valid examples include:"
echo "${_indent}linux-x64 and linux-arm64. Default is linux-x64 if unspecified."
echo "${_indent}See also: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog"
echo
echo "${_indent}-${_KIND_ARG}, --${_KIND_ARG_NAME} value"
echo "${_indent}Package output kind. Value must be one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}."
echo "${_indent}Default is ${_APPIMAGE_KIND} if unspecified."
echo
echo "${_indent}-${_VERBOSE_ARG}, --${_VERBOSE_ARG_NAME}"
echo "${_indent}Verbose review info output flag."
echo
echo "${_indent}-${_RUNAPP_ARG}, --${_RUNAPP_ARG_NAME}"
echo "${_indent}Run the application after successful build flag."
echo
echo "${_indent}-${_SKIPYES_ARG}, --${_SKIPYES_ARG_NAME}"
echo "${_indent}Skip confirmation prompt flag (assumes yes)."
echo
echo "${_indent}-${_OUTPUT_ARG}, --${_OUTPUT_ARG_NAME}"
echo "${_indent}Explicit final output filename (excluding directory part)."
echo
echo "Example:"
echo "${_indent}${_SCRIPT_NAME} -${_RID_ARG} linux-arm64"
echo
exit $_exit_help
fi
if [ ${_version_arg_value} == true ]; then
echo
echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}"
echo "${_SCRIPT_COPYRIGHT}"
echo "${_SCRIPT_WEBSITE}"
echo
echo "MIT License"
echo
echo "Permission is hereby granted, free of charge, to any person obtaining a copy"
echo "of this software and associated documentation files (the "Software"), to deal"
echo "in the Software without restriction, including without limitation the rights"
echo "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell"
echo "copies of the Software, and to permit persons to whom the Software is"
echo "furnished to do so, subject to the following conditions:"
echo
echo "The above copyright notice and this permission notice shall be included in all"
echo "copies or substantial portions of the Software."
echo
echo "THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR"
echo "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,"
echo "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE"
echo "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER"
echo "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,"
echo "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE"
echo "SOFTWARE."
echo
exit 0
fi
###############################
# SOURCE & WORKING
###############################
# Export these now as may be
# useful in an advanced config file
export DOTNET_RID="${_rid_arg_value}"
export PKG_KIND="${_kind_arg_value}"
export ISO_DATE=`date +"%Y-%m-%d"`
if [ ! -f "${_conf_arg_value}" ]; then
echo "Configuration file not found: ${_conf_arg_value}"
echo
exit 1
fi
# Export contents to any post publish command
set -a
# Source local to PWD
source "${_conf_arg_value}"
set +a
# For AppImage tool and backward compatibility
export VERSION="${APP_VERSION}"
# Then change PWD to conf file
cd "$(dirname "${_conf_arg_value}")"
###############################
# SANITY
###############################
if (( ${CONF_IMPL_VERSION} < ${_SCRIPT_IMPL_MIN} )) || (( ${CONF_IMPL_VERSION} > ${_SCRIPT_IMPL_MAX} )); then
echo "Configuration format version ${_SCRIPT_IMPL_VERSION} not compatible"
echo "Older conf file but newer ${_SCRIPT_NAME} implementation?"
echo "Update from: ${_SCRIPT_WEBSITE}"
echo
exit 1
fi
assert_mandatory "APP_MAIN" "${APP_MAIN}"
assert_mandatory "APP_ID" "${APP_ID}"
assert_mandatory "APP_ICON_SRC" "${APP_ICON_SRC}"
assert_mandatory "DE_NAME" "${DE_NAME}"
assert_mandatory "DE_CATEGORIES" "${DE_CATEGORIES}"
assert_mandatory "PKG_OUTPUT_DIR" "${PKG_OUTPUT_DIR}"
if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
assert_mandatory "APPIMAGETOOL_COMMAND" "${APPIMAGETOOL_COMMAND}"
fi
assert_opt_file "APP_ICON_SRC" "${APP_ICON_SRC}"
assert_opt_file "APP_XML_SRC" "${APP_XML_SRC}"
if [ "${DE_TERMINAL_FLAG}" != "true" ] && [ "${DE_TERMINAL_FLAG}" != "false" ]; then
echo "DE_TERMINAL_FLAG invalid value: ${DE_TERMINAL_FLAG}"
echo
exit 1
fi
if [ "${DOTNET_PROJECT_PATH}" == "${_DOTNET_NONE}" ] && [ "${POST_PUBLISH}" == "" ]; then
echo "No publish or build operation defined (nothing will be built)"
echo "See DOTNET_PROJECT_PATH and POST_PUBLISH in: ${_conf_arg_value}"
echo
exit 1
fi
if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ] &&
[ ! -f "${DOTNET_PROJECT_PATH}" ] && [ ! -d "${DOTNET_PROJECT_PATH}" ]; then
echo "DOTNET_PROJECT_PATH path not found: ${DOTNET_PROJECT_PATH}"
echo
exit 1
fi
if [ "${_kind_arg_value}" != "${_APPIMAGE_KIND}" ] && [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then
echo "Invalid argument value: ${_kind_arg_value}"
echo "Use one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}"
echo
exit 1
fi
# Detect if publish for windows
_exec_ext=""
declare -l _tw="${_rid_arg_value}"
if [[ "${_tw}" == "win"* ]]; then
# May use this in future
_exec_ext=".exe"
if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
echo "Invalid AppImage payload"
echo "Looks like a windows binary to be packaged as AppImage."
echo "Use --${_KIND_ARG_NAME} ${_ZIP_KIND} instead."
echo
exit 1
fi
fi
###############################
# VARIABLES
###############################
# Abbreviate RID where it maps well to arch
if [ "${_rid_arg_value}" == "linux-x64" ]; then
_file_out_arch="-x86_64"
elif [ "${_rid_arg_value}" == "linux-arm64" ]; then
_file_out_arch="-aarch64"
else
# Otherwise use RID itself
_file_out_arch="-${_rid_arg_value}"
fi
# APPDIR LOCATIONS
export APPDIR_ROOT="${PKG_OUTPUT_DIR}/AppDir"
if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
# AppImage
export APPDIR_USR="${APPDIR_ROOT}/usr"
export APPDIR_BIN="${APPDIR_ROOT}/usr/bin"
export APPDIR_SHARE="${APPDIR_ROOT}/usr/share"
_local_run="usr/bin/${APP_MAIN}${_exec_ext}"
else
# Simple zip
export APPDIR_USR=""
export APPDIR_BIN="${APPDIR_ROOT}"
export APPDIR_SHARE="${APPDIR_ROOT}"
_local_run="${APP_MAIN}${_exec_ext}"
fi
export APPRUN_TARGET="${APPDIR_BIN}/${APP_MAIN}${_exec_ext}"
# DOTNET PUBLISH
if [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ]; then
_publish_cmd="dotnet publish"
if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "." ]; then
_publish_cmd="${_publish_cmd} \"${DOTNET_PROJECT_PATH}\""
fi
_publish_cmd="${_publish_cmd} -r ${_rid_arg_value}"
if [ "${APP_VERSION}" != "" ]; then
_publish_cmd="${_publish_cmd} -p:Version=${APP_VERSION}"
fi
if [ "${DOTNET_PUBLISH_ARGS}" != "" ]; then
_publish_cmd="${_publish_cmd} ${DOTNET_PUBLISH_ARGS}"
fi
_publish_cmd="${_publish_cmd} -o \"${APPDIR_BIN}\""
fi
# PACKAGE OUTPUT
if [ $PKG_VERSION_FLAG == true ] && [ "${APP_VERSION}" != "" ]; then
_version_out="-${APP_VERSION}"
fi
if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
# AppImageTool
if [ "${_output_arg_value}" != "" ]; then
_package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}"
else
_package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}${PKG_APPIMAGE_SUFFIX}"
fi
_package_cmd="${APPIMAGETOOL_COMMAND}"
if [ "${PKG_APPIMAGE_ARGS}" != "" ]; then
_package_cmd="${_package_cmd} ${PKG_APPIMAGE_ARGS}"
fi
_package_cmd="${_package_cmd} \"${APPDIR_ROOT}\" \"${_package_out}\""
if [ ${_runapp_arg_value} == true ]; then
_packrun_cmd="${_package_out}"
fi
else
# Simple zip
if [ "${_output_arg_value}" != "" ]; then
_package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}"
else
_package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}.zip"
fi
_package_cmd="(cd \"${APPDIR_ROOT}\" && zip -r \"${PWD}/${_package_out}\" ./)"
if [ ${_runapp_arg_value} == true ]; then
_packrun_cmd="${APPRUN_TARGET}"
fi
fi
###############################
# DESKTOP ENTRY & APPDATA
###############################
if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
_desktop="[Desktop Entry]\n"
_desktop="${_desktop}Type=Application\n"
_desktop="${_desktop}Name=${DE_NAME}\n"
_desktop="${_desktop}Exec=AppRun\n"
_desktop="${_desktop}Terminal=${DE_TERMINAL_FLAG}\n"
_desktop="${_desktop}Categories=${DE_CATEGORIES}\n"
# Follow app-id
_desktop="${_desktop}Icon=${APP_ID}\n"
if [ "${DE_COMMENT}" != "" ]; then
_desktop="${_desktop}Comment=${DE_COMMENT}\n"
fi
if [ "${DE_KEYWORDS}" != "" ]; then
_desktop="${_desktop}Keywords=${DE_KEYWORDS}\n"
fi
_desktop="${_desktop}${DE_EXTEND}\n"
fi
# Load appdata.xml
if [ "${APP_XML_SRC}" != "" ]; then
if command -v envsubst &> /dev/null; then
_appxml=$(envsubst <"${APP_XML_SRC}")
else
_appxml=$(<"${APP_XML_SRC}")
echo "WARNING: Variable substitution not available for: ${APP_XML_SRC}"
echo
fi
fi
###############################
# DISPLAY & CONFIRM
###############################
echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}"
echo "${_SCRIPT_COPYRIGHT}"
echo
echo "APP_MAIN: ${APP_MAIN}"
echo "APP_ID: ${APP_ID}"
echo "APP_VERSION: ${APP_VERSION}"
echo "OUTPUT: ${_package_out}"
echo
if [ "${_desktop}" != "" ]; then
echo -e "${_desktop}"
fi
if [ ${_verbose_arg_value} == true ] && [ "${_appxml}" != "" ]; then
echo -e "${_appxml}\n"
fi
echo "Build Commands:"
if [ "${_publish_cmd}" != "" ]; then
echo
echo "${_publish_cmd}"
fi
if [ "${POST_PUBLISH}" != "" ]; then
echo
echo "${POST_PUBLISH}"
fi
echo
echo "${_package_cmd}"
echo
# Prompt
if [ $_skipyes_arg_value == false ]; then
echo
read -p "Build now [N/y]? " prompt
if [ "${prompt}" != "y" ] && [ "${prompt}" != "Y" ]; then
echo
exit 1
fi
# Continue
echo
fi
###############################
# PUBLISH & BUILD
###############################
# Clean and ensure directoy exists
ensure_directory "${PKG_OUTPUT_DIR}"
remove_path "${APPDIR_ROOT}"
remove_path "${_package_out}"
# Create AppDir structure
ensure_directory "${APPDIR_BIN}"
if [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then
# We also create usr/share/icons, as some packages require this.
# See: https://github.com/kuiperzone/Publish-AppImage/issues/7
ensure_directory "${APPDIR_SHARE}/icons"
fi
echo
# Publish dotnet
if [ "${_publish_cmd}" != "" ]; then
exec_or_die "${_publish_cmd}"
echo
fi
# Post-publish
if [ "${POST_PUBLISH}" != "" ]; then
exec_or_die "${POST_PUBLISH}"
echo
fi
# Application file must exist!
if [ ! -f "${APPRUN_TARGET}" ]; then
echo "Expected application file not found: ${APPRUN_TARGET}"
echo
exit 1
fi
if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
echo
# Create desktop
if [ "${_desktop}" != "" ]; then
_file="${APPDIR_ROOT}/${APP_ID}.desktop"
echo "Creating: ${_file}"
echo -e "${_desktop}" > "${_file}"
assert_result
fi
if [ "${_appxml}" != "" ]; then
_dir="${APPDIR_SHARE}/metainfo"
_file="${_dir}/${APP_ID}.appdata.xml"
echo "Creating: ${_file}"
ensure_directory "${_dir}"
echo -e "${_appxml}" > "${_file}"
assert_result
if [ "${_desktop}" != "" ]; then
# Copy of desktop under "applications"
# Needed for launchable in appinfo.xml (if used)
# See https://github.com/AppImage/AppImageKit/issues/603
_dir="${APPDIR_SHARE}/applications"
_file="${_dir}/${APP_ID}.desktop"
echo "Creating: ${_file}"
ensure_directory "${_dir}"
echo -e "${_desktop}" > "${_file}"
assert_result
fi
fi
# Copy icon
if [ "${APP_ICON_SRC}" != "" ]; then
_icon_ext="${APP_ICON_SRC##*.}"
if [ "${_icon_ext}" != "" ]; then
_icon_ext=".${_icon_ext}"
fi
_temp="${APPDIR_ROOT}/${APP_ID}${_icon_ext}"
echo "Creating: ${_temp}"
cp "${APP_ICON_SRC}" "${_temp}"
assert_result
fi
# AppRun
_temp="${APPDIR_ROOT}/AppRun"
if [ ! -f "${_temp}" ]; then
echo "Creating: ${_temp}"
ln -s "${_local_run}" "${_temp}"
assert_result
fi
fi
# Build package
echo
exec_or_die "${_package_cmd}"
echo
echo "OUTPUT OK: ${_package_out}"
echo
if [ "${_packrun_cmd}" != "" ]; then
echo "RUNNING ..."
exec_or_die "${_packrun_cmd}"
echo
fi
exit 0

View file

@ -0,0 +1,140 @@
################################################################################
# BASH FORMAT CONFIG: Publish-AppImage for .NET
# WEBPAGE : https://kuiper.zone/publish-appimage-dotnet/
################################################################################
########################################
# Application
########################################
# Mandatory application (file) name. This must be the base name of the main
# runnable file to be created by the publish/build process. It should NOT
# include any directory part or extension, i.e. do not append ".exe" or ".dll"
# for dotnet. Example: "MyApp"
APP_MAIN="sourcegit"
# Mandatory application ID in reverse DNS form, i.e. "tld.my-domain.MyApp".
# Exclude any ".desktop" post-fix. Note that reverse DNS form is necessary
# for compatibility with Freedesktop.org metadata.
APP_ID="com.sourcegit-scm.SourceGit"
# Mandatory icon source file relative to this file (appimagetool seems to
# require this). Use .svg or .png only. PNG should be one of standard sizes,
# i.e, 128x128 or 256x256 pixels. Example: "Assets/app.svg"
APP_ICON_SRC="sourcegit.png"
# Optional Freedesktop.org metadata source file relative to this file. It is not essential
# (leave empty) but will be used by appimagetool for repository information if provided.
# See for information: https://docs.appimage.org/packaging-guide/optional/appstream.html
# NB. The file may embed bash variables defined in this file and those listed below
# (these will be substituted during the build). Examples include: "<id>${APP_ID}</id>"
# and "<release version="${APP_VERSION}" date="${ISO_DATE}">".
# $ISO_DATE : date of build, i.e. "2021-10-29",
# $APP_VERSION : application version (if provided),
# Example: "Assets/appdata.xml".
APP_XML_SRC="sourcegit.appdata.xml"
########################################
# Desktop Entry
########################################
# Mandatory friendly name of the application.
DE_NAME="SourceGit"
# Mandatory category(ies), separated with semicolon, in which the entry should be
# shown. See https://specifications.freedesktop.org/menu-spec/latest/apa.html
# Examples: "Development", "Graphics", "Network", "Utility" etc.
DE_CATEGORIES="Utility"
# Optional short comment text (single line).
# Example: "Perform calculations"
DE_COMMENT="Open-source GUI client for git users"
# Optional keywords, separated with semicolon. Values are not meant for
# display and should not be redundant with the value of DE_NAME.
DE_KEYWORDS=""
# Flag indicating whether the program runs in a terminal window. Use true or false only.
DE_TERMINAL_FLAG=false
# Optional name-value text to be appended to the Desktop Entry file, thus providing
# additional metadata. Name-values should not be redundant with values above and
# are to be terminated with new line ("\n").
# Example: "Comment[fr]=Effectue des calculs compliqués\nMimeType=image/x-foo"
DE_EXTEND=""
########################################
# Dotnet Publish
########################################
# Optional path relative to this file in which to find the dotnet project (.csproj)
# or solution (.sln) file, or the directory containing it. If empty (default), a single
# project or solution file is expected under the same directory as this file.
# IMPORTANT. If set to "null", dotnet publish is disabled (it is NOT called). Instead,
# only POST_PUBLISH is called. Example: "Source/MyProject"
DOTNET_PROJECT_PATH="../../../src/SourceGit.csproj"
# Optional arguments suppled to "dotnet publish". Do NOT include "-r" (runtime) or version here as they will
# be added (see also $APP_VERSION). Typically you want as a minimum: "-c Release --self-contained true".
# Additional useful arguments include:
# "-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=link"
# Refer: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish
DOTNET_PUBLISH_ARGS="-c Release -p:DebugType=None -p:DebugSymbols=false"
########################################
# POST-PUBLISH
########################################
# Optional post-publish or standalone build command. The value could, for example, copy
# additional files into the "bin" directory. The working directory will be the location
# of this file. The value is mandatory if DOTNET_PROJECT_PATH equals "null". In
# addition to variables in this file, the following variables are exported prior:
# $ISO_DATE : date of build, i.e. "2021-10-29",
# $APP_VERSION : application version (if provided),
# $DOTNET_RID : dotnet runtime identifier string provided at command line (i.e. "linux-x64),
# $PKG_KIND : package kind (i.e. "appimage", "zip") provided at command line.
# $APPDIR_ROOT : AppImage build directory root (i.e. "AppImages/AppDir").
# $APPDIR_USR : AppImage user directory under root (i.e. "AppImages/AppDir/usr").
# $APPDIR_BIN : AppImage bin directory under root (i.e. "AppImages/AppDir/usr/bin").
# $APPRUN_TARGET : The expected target executable file (i.e. "AppImages/AppDir/usr/bin/app-name").
# Example: "Assets/post-publish.sh"
POST_PUBLISH="mv AppImages/AppDir/usr/bin/SourceGit AppImages/AppDir/usr/bin/sourcegit; rm -f AppImages/AppDir/usr/bin/*.dbg"
########################################
# Package Output
########################################
# Additional arguments for use with appimagetool. See appimagetool --help.
# Default is empty. Example: "--sign"
PKG_APPIMAGE_ARGS="--runtime-file=runtime-x86_64"
# Mandatory output directory relative to this file. It will be created if it does not
# exist. It will contain the final package file and temporary AppDir. Default: "AppImages".
PKG_OUTPUT_DIR="AppImages"
# Boolean which sets whether to include the application version in the filename of the final
# output package (i.e. "HelloWorld-1.2.3-x86_64.AppImage"). It is ignored if $APP_VERSION is
# empty or the "output" command arg is specified. Default and recommended: false.
PKG_VERSION_FLAG=false
# Optional AppImage output filename extension. It is ignored if generating a zip file, or if
# the "output" command arg is specified. Default and recommended: ".AppImage".
PKG_APPIMAGE_SUFFIX=".AppImage"
########################################
# Advanced Other
########################################
# The appimagetool command. Default is "appimagetool" which is expected to be found
# in the $PATH. If the tool is not in path or has different name, a full path can be given,
# example: "/home/user/Apps/appimagetool-x86_64.AppImage"
APPIMAGETOOL_COMMAND="appimagetool"
# Internal use only. Used for compatibility between conf and script. Do not modify.
CONF_IMPL_VERSION=1

Binary file not shown.

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.sourcegit_scm.SourceGit</id>
<id>com.sourcegit-scm.SourceGit</id>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<name>SourceGit</name>
@ -8,9 +8,8 @@
<description>
<p>Open-source GUI client for git users</p>
</description>
<url type="homepage">https://github.com/sourcegit-scm/sourcegit</url>
<launchable type="desktop-id">com.sourcegit_scm.SourceGit.desktop</launchable>
<launchable type="desktop-id">com.sourcegit-scm.SourceGit.desktop</launchable>
<provides>
<id>com.sourcegit_scm.SourceGit.desktop</id>
<id>com.sourcegit-scm.SourceGit.desktop</id>
</provides>
</component>
</component>

View file

@ -1,8 +1,7 @@
Package: sourcegit
Version: 2025.10
Version: 8.18
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

@ -0,0 +1,5 @@
#!bin/sh
echo 'Create link on /usr/bin'
ln -s /opt/sourcegit/sourcegit /usr/bin/sourcegit
exit 0

View file

@ -0,0 +1,4 @@
#!bin/sh
rm -f /usr/bin/sourcegit
exit 0

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.so.6
Requires: libSM.so.6
%define _build_id_links none
@ -16,23 +14,24 @@ Requires: xdg-utils
Open-source & Free Git Gui Client
%install
mkdir -p %{buildroot}/opt/sourcegit
mkdir -p %{buildroot}/%{_bindir}
mkdir -p %{buildroot}/usr/share/applications
mkdir -p %{buildroot}/usr/share/icons
cp -f ../../../SourceGit/* %{buildroot}/opt/sourcegit/
ln -rsf %{buildroot}/opt/sourcegit/sourcegit %{buildroot}/%{_bindir}
cp -r ../../_common/applications %{buildroot}/%{_datadir}
cp -r ../../_common/icons %{buildroot}/%{_datadir}
chmod 755 -R %{buildroot}/opt/sourcegit
chmod 755 %{buildroot}/%{_datadir}/applications/sourcegit.desktop
mkdir -p $RPM_BUILD_ROOT/opt/sourcegit
mkdir -p $RPM_BUILD_ROOT/usr/share/applications
mkdir -p $RPM_BUILD_ROOT/usr/share/icons
cp -r ../../_common/applications $RPM_BUILD_ROOT/usr/share/
cp -r ../../_common/icons $RPM_BUILD_ROOT/usr/share/
cp -f ../../../SourceGit/* $RPM_BUILD_ROOT/opt/sourcegit/
chmod 755 -R $RPM_BUILD_ROOT/opt/sourcegit
chmod 755 $RPM_BUILD_ROOT/usr/share/applications/sourcegit.desktop
%files
%dir /opt/sourcegit/
/opt/sourcegit/*
/usr/share/applications/sourcegit.desktop
/usr/share/icons/*
%{_bindir}/sourcegit
/opt/sourcegit
/usr/share
%post
ln -s /opt/sourcegit/sourcegit /usr/bin/sourcegit
%postun
rm -f /usr/bin/sourcegit
%changelog
# skip
# skip

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

@ -1,70 +0,0 @@
#!/usr/bin/env bash
set -e
set -o
set -u
set pipefail
arch=
appimage_arch=
target=
case "$RUNTIME" in
linux-x64)
arch=amd64
appimage_arch=x86_64
target=x86_64;;
linux-arm64)
arch=arm64
appimage_arch=arm_aarch64
target=aarch64;;
*)
echo "Unknown runtime $RUNTIME"
exit 1;;
esac
APPIMAGETOOL_URL=https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
cd build
if [[ ! -f "appimagetool" ]]; then
curl -o appimagetool -L "$APPIMAGETOOL_URL"
chmod +x appimagetool
fi
rm -f SourceGit/*.dbg
mkdir -p SourceGit.AppDir/opt
mkdir -p SourceGit.AppDir/usr/share/metainfo
mkdir -p SourceGit.AppDir/usr/share/applications
cp -r SourceGit SourceGit.AppDir/opt/sourcegit
desktop-file-install resources/_common/applications/sourcegit.desktop --dir SourceGit.AppDir/usr/share/applications \
--set-icon com.sourcegit_scm.SourceGit --set-key=Exec --set-value=AppRun
mv SourceGit.AppDir/usr/share/applications/{sourcegit,com.sourcegit_scm.SourceGit}.desktop
cp resources/appimage/sourcegit.png SourceGit.AppDir/com.sourcegit_scm.SourceGit.png
ln -rsf SourceGit.AppDir/opt/sourcegit/sourcegit SourceGit.AppDir/AppRun
ln -rsf SourceGit.AppDir/usr/share/applications/com.sourcegit_scm.SourceGit.desktop SourceGit.AppDir
cp resources/appimage/sourcegit.appdata.xml SourceGit.AppDir/usr/share/metainfo/com.sourcegit_scm.SourceGit.appdata.xml
ARCH="$appimage_arch" ./appimagetool -v SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage"
mkdir -p resources/deb/opt/sourcegit/
mkdir -p resources/deb/usr/bin
mkdir -p resources/deb/usr/share/applications
mkdir -p resources/deb/usr/share/icons
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"
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./

View file

@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -e
set -o
set -u
set pipefail
cd build
mkdir -p SourceGit.app/Contents/Resources
mv SourceGit SourceGit.app/Contents/MacOS
cp resources/app/App.icns SourceGit.app/Contents/Resources/App.icns
sed "s/SOURCE_GIT_VERSION/$VERSION/g" resources/app/App.plist > SourceGit.app/Contents/Info.plist
rm -rf SourceGit.app/Contents/MacOS/SourceGit.dsym
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit.app

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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 KiB

After

Width:  |  Height:  |  Size: 329 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Before After
Before After

View file

@ -1,58 +0,0 @@
using System;
using System.Windows.Input;
using Avalonia.Controls;
namespace SourceGit
{
public partial class App
{
public class Command : ICommand
{
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
public Command(Action<object> action)
{
_action = action;
}
public bool CanExecute(object parameter) => _action != null;
public void Execute(object parameter) => _action?.Invoke(parameter);
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 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 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);
});
}
}

View file

@ -20,6 +20,20 @@ namespace SourceGit
}
}
public class FontFamilyConverter : JsonConverter<FontFamily>
{
public override FontFamily Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var name = reader.GetString();
return new FontFamily(name);
}
public override void Write(Utf8JsonWriter writer, FontFamily value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
public class GridLengthConverter : JsonConverter<GridLength>
{
public override GridLength Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@ -40,15 +54,15 @@ namespace SourceGit
IgnoreReadOnlyProperties = true,
Converters = [
typeof(ColorConverter),
typeof(FontFamilyConverter),
typeof(GridLengthConverter),
]
)]
[JsonSerializable(typeof(Models.ExternalToolPaths))]
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
[JsonSerializable(typeof(Models.JetBrainsState))]
[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

@ -13,21 +13,15 @@
<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>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="/Resources/Styles.axaml"/>
</Application.Styles>
@ -35,11 +29,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.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Preference}" Command="{x:Static s:App.OpenPreferenceCommand}" Gesture="⌘+,"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>
</NativeMenu>

View file

@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
@ -17,20 +15,35 @@ using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
namespace SourceGit
{
public class SimpleCommand : ICommand
{
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
public SimpleCommand(Action action)
{
_action = action;
}
public bool CanExecute(object parameter) => _action != null;
public void Execute(object parameter) => _action?.Invoke();
private Action _action = null;
}
public partial class App : Application
{
#region App Entry Point
[STAThread]
public static void Main(string[] args)
{
Native.OS.SetupDataDir();
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
LogException(e.ExceptionObject as Exception);
@ -38,14 +51,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);
@ -61,11 +75,6 @@ namespace SourceGit
var builder = AppBuilder.Configure<App>();
builder.UsePlatformDetect();
builder.LogToTrace();
builder.WithInterFont();
builder.With(new FontManagerOptions()
{
DefaultFamilyName = "fonts:Inter#Inter"
});
builder.ConfigureFonts(manager =>
{
var monospace = new EmbeddedFontCollection(
@ -78,72 +87,42 @@ namespace SourceGit
return builder;
}
public static void LogException(Exception ex)
public static readonly SimpleCommand OpenPreferenceCommand = new SimpleCommand(() =>
{
if (ex == null)
var toplevel = GetTopLevel() as Window;
if (toplevel == null)
return;
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
builder.Append($"User: {Environment.UserName}\n");
builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
builder.Append($"Exception Time: {DateTime.Now}\n");
builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex);
var dialog = new Views.Preference();
dialog.ShowDialog(toplevel);
});
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
File.WriteAllText(file, builder.ToString());
}
#endregion
#region Utility Functions
public static void ShowWindow(object data, bool showAsDialog)
public static readonly SimpleCommand OpenHotkeysCommand = new SimpleCommand(() =>
{
var impl = (Views.ChromelessWindow target, bool isDialog) =>
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
if (isDialog)
target.ShowDialog(owner);
else
target.Show(owner);
}
else
{
target.Show();
}
};
if (data is Views.ChromelessWindow window)
{
impl(window, showAsDialog);
return;
}
var dataTypeName = data.GetType().FullName;
if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal))
var toplevel = GetTopLevel() as Window;
if (toplevel == null)
return;
var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
var viewType = Type.GetType(viewTypeName);
if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow)))
var dialog = new Views.Hotkeys();
dialog.ShowDialog(toplevel);
});
public static readonly SimpleCommand OpenAboutCommand = new SimpleCommand(() =>
{
var toplevel = GetTopLevel() as Window;
if (toplevel == null)
return;
window = Activator.CreateInstance(viewType) as Views.ChromelessWindow;
if (window != null)
{
window.DataContext = data;
impl(window, showAsDialog);
}
}
var dialog = new Views.About();
dialog.ShowDialog(toplevel);
});
public static readonly SimpleCommand CheckForUpdateCommand = new SimpleCommand(() =>
{
Check4Update(true);
});
public static readonly SimpleCommand QuitCommand = new SimpleCommand(() => Quit(0));
public static void RaiseException(string context, string message)
{
@ -160,10 +139,7 @@ namespace SourceGit
public static void SetLocale(string localeKey)
{
var app = Current as App;
if (app == null)
return;
var targetLocale = app.Resources[localeKey] as ResourceDictionary;
var targetLocale = app?.Resources[localeKey] as ResourceDictionary;
if (targetLocale == null || targetLocale == app._activeLocale)
return;
@ -228,67 +204,12 @@ namespace SourceGit
}
}
public static void SetFonts(string defaultFont, string monospaceFont, bool onlyUseMonospaceFontInEditor)
{
var app = Current as App;
if (app == null)
return;
if (app._fontsOverrides != null)
{
app.Resources.MergedDictionaries.Remove(app._fontsOverrides);
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));
if (string.IsNullOrEmpty(monospaceFont))
{
if (!string.IsNullOrEmpty(defaultFont))
{
monospaceFont = $"fonts:SourceGit#JetBrains Mono,{defaultFont}";
resDic.Add("Fonts.Monospace", new FontFamily(monospaceFont));
}
}
else
{
if (!string.IsNullOrEmpty(defaultFont) && !monospaceFont.Contains(defaultFont, StringComparison.Ordinal))
monospaceFont = $"{monospaceFont},{defaultFont}";
resDic.Add("Fonts.Monospace", new FontFamily(monospaceFont));
}
if (onlyUseMonospaceFontInEditor)
{
if (string.IsNullOrEmpty(defaultFont))
resDic.Add("Fonts.Primary", new FontFamily("fonts:Inter#Inter"));
else
resDic.Add("Fonts.Primary", new FontFamily(defaultFont));
}
else
{
if (!string.IsNullOrEmpty(monospaceFont))
resDic.Add("Fonts.Primary", new FontFamily(monospaceFont));
}
if (resDic.Count > 0)
{
app.Resources.MergedDictionaries.Add(resDic);
app._fontsOverrides = resDic;
}
}
public static async void CopyText(string data)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow?.Clipboard is { } clipboard)
await clipboard.SetTextAsync(data ?? "");
await clipboard.SetTextAsync(data);
}
}
@ -301,7 +222,7 @@ namespace SourceGit
return await clipboard.GetTextAsync();
}
}
return null;
return default;
}
public static string Text(string key, params object[] args)
@ -323,25 +244,84 @@ 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;
}
public static IStorageProvider GetStorageProvider()
public static TopLevel GetTopLevel()
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
return desktop.MainWindow?.StorageProvider;
{
return desktop.MainWindow;
}
return null;
}
public static ViewModels.Launcher GetLauncher()
public static void Check4Update(bool manually = false)
{
Task.Run(async () =>
{
try
{
// Fetch lastest release information.
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) };
var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json");
// Parse json into Models.Version.
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
if (ver == null)
return;
// Check if already up-to-date.
if (!ver.IsNewVersion)
{
if (manually)
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
return;
}
// Should not check ignored tag if this is called manually.
if (!manually)
{
var pref = ViewModels.Preference.Instance;
if (ver.TagName == pref.IgnoreUpdateTag)
return;
}
ShowSelfUpdateResult(ver);
}
catch (Exception e)
{
if (manually)
ShowSelfUpdateResult(e);
}
});
}
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,19 +334,15 @@ namespace SourceGit
Environment.Exit(exitCode);
}
}
#endregion
#region Overrides
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
var pref = ViewModels.Preferences.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
var pref = ViewModels.Preference.Instance;
SetLocale(pref.Locale);
SetTheme(pref.Theme, pref.ThemeOverrides);
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor);
}
public override void OnFrameworkInitializationCompleted()
@ -375,47 +351,59 @@ namespace SourceGit
{
BindingPlugins.DataValidators.RemoveAt(0);
// Disable tooltip if window is not active.
ToolTip.ToolTipOpeningEvent.AddClassHandler<Control>((c, e) =>
{
var topLevel = TopLevel.GetTopLevel(c);
if (topLevel is not Window { IsActive: true })
e.Cancel = true;
});
if (TryLaunchAsCoreEditor(desktop))
if (TryLaunchedAsCoreEditor(desktop))
return;
if (TryLaunchAsAskpass(desktop))
if (TryLaunchedAsAskpass(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))
{
if (arg.StartsWith('"') && arg.EndsWith('"'))
arg = arg.Substring(1, arg.Length - 2).Trim();
if (arg.Length > 0 && !Path.IsPathFullyQualified(arg))
arg = Path.GetFullPath(arg);
}
_ipcChannel.SendToFirstInstance(arg);
Environment.Exit(0);
}
else
{
_ipcChannel.MessageReceived += TryOpenRepository;
desktop.Exit += (_, _) => _ipcChannel.Dispose();
TryLaunchAsNormal(desktop);
}
TryLaunchedAsNormal(desktop);
}
}
#endregion
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
private static void LogException(Exception ex)
{
if (ex == null)
return;
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion.ToString()}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex.StackTrace);
while (ex.InnerException != null)
{
ex = ex.InnerException;
builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n");
builder.Append(ex.StackTrace);
}
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 ShowSelfUpdateResult(object data)
{
Dispatcher.UIThread.Post(() =>
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: not null } desktop)
{
var dialog = new Views.SelfUpdate()
{
DataContext = new ViewModels.SelfUpdate() { Data = data }
};
dialog.Show(desktop.MainWindow);
}
});
}
private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -468,57 +456,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,181 +496,49 @@ 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")
var args = desktop.Args;
if (args == null || args.Length != 1)
return false;
var args = desktop.Args;
if (args?.Length > 0)
{
var askpass = new Views.Askpass();
askpass.TxtDescription.Text = args[0];
desktop.MainWindow = askpass;
return true;
}
var param = args[0];
if (!param.StartsWith("enter passphrase", StringComparison.OrdinalIgnoreCase) &&
!param.Contains(" password", StringComparison.OrdinalIgnoreCase))
return false;
return false;
desktop.MainWindow = new Views.Askpass(param);
return true;
}
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{
Native.OS.SetupExternalTools();
Models.AvatarManager.Instance.Start();
Native.OS.SetupEnternalTools();
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())
{
pref.Save();
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;
private ResourceDictionary _fontsOverrides = 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,26 +1,31 @@
namespace SourceGit.Commands
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Add : Command
{
public Add(string repo, bool includeUntracked)
public Add(string repo, List<Models.Change> changes = null)
{
WorkingDirectory = repo;
Context = repo;
Args = includeUntracked ? "add ." : "add -u .";
}
public Add(string repo, Models.Change change)
{
WorkingDirectory = repo;
Context = repo;
Args = $"add -- \"{change.Path}\"";
}
public Add(string repo, string pathspecFromFile)
{
WorkingDirectory = repo;
Context = repo;
Args = $"add --pathspec-from-file=\"{pathspecFromFile}\"";
if (changes == null || changes.Count == 0)
{
Args = "add .";
}
else
{
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,43 +12,25 @@ 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 --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 --progress -b {branch} {basedOn}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
return Exec();
}
public bool UseTheirs(List<string> files)
{
var builder = new StringBuilder();
StringBuilder builder = new StringBuilder();
builder.Append("checkout --theirs --");
foreach (var f in files)
{
@ -61,7 +44,7 @@ namespace SourceGit.Commands
public bool UseMine(List<string> files)
{
var builder = new StringBuilder();
StringBuilder builder = new StringBuilder();
builder.Append("checkout --ours --");
foreach (var f in files)
{
@ -75,8 +58,37 @@ namespace SourceGit.Commands
public bool FileWithRevision(string file, string revision)
{
Args = $"checkout --no-overlay {revision} -- \"{file}\"";
Args = $"checkout {revision} -- \"{file}\"";
return Exec();
}
public bool Commit(string commitId, Action<string> onProgress)
{
Args = $"checkout --detach --progress {commitId}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
return Exec();
}
public bool Files(List<string> files)
{
StringBuilder builder = new StringBuilder();
builder.Append("checkout -f -q --");
foreach (var f in files)
{
builder.Append(" \"");
builder.Append(f);
builder.Append("\"");
}
Args = builder.ToString();
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -2,19 +2,12 @@
{
public class CherryPick : Command
{
public CherryPick(string repo, string commits, bool noCommit, bool appendSourceToMessage, string extraParams)
public CherryPick(string repo, string commit, bool noCommit)
{
var mode = noCommit ? "-n" : "--ff";
WorkingDirectory = repo;
Context = repo;
Args = "cherry-pick ";
if (noCommit)
Args += "-n ";
if (appendSourceToMessage)
Args += "-x ";
if (!string.IsNullOrEmpty(extraParams))
Args += $"{extraParams} ";
Args += commits;
Args = $"cherry-pick {mode} {commit}";
}
}
}

View file

@ -1,4 +1,7 @@
namespace SourceGit.Commands
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Clean : Command
{
@ -6,7 +9,23 @@
{
WorkingDirectory = repo;
Context = repo;
Args = "clean -qfdx";
Args = "clean -qfd";
}
public Clean(string repo, List<string> files)
{
StringBuilder 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,11 +10,15 @@ 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;
public string StdOut { get; set; } = "";
public string StdErr { get; set; } = "";
public bool IsSuccess { get; set; }
public string StdOut { get; set; }
}
public enum EditorType
@ -26,122 +29,19 @@ 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 };
proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs);
proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs);
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);
return false;
}
proc.BeginOutputReadLine();
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 (RaiseError)
{
var errMsg = string.Join("\n", errs).Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg));
}
return false;
}
return true;
}
public ReadToEndResult ReadToEnd()
{
var start = CreateGitStartInfo();
var proc = new Process() { StartInfo = start };
try
{
proc.Start();
}
catch (Exception e)
{
return new ReadToEndResult()
{
IsSuccess = false,
StdOut = string.Empty,
StdErr = e.Message,
};
}
var rs = new ReadToEndResult()
{
StdOut = proc.StandardOutput.ReadToEnd(),
StdErr = proc.StandardError.ReadToEnd(),
};
proc.WaitForExit();
rs.IsSuccess = proc.ExitCode == 0;
proc.Close();
return rs;
}
private ProcessStartInfo CreateGitStartInfo()
{
var start = new ProcessStartInfo();
start.FileName = Native.OS.GitExecutable;
start.Arguments = "--no-pager -c core.quotepath=off -c credential.helper=manager ";
start.Arguments = "--no-pager -c core.quotepath=off ";
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
@ -155,18 +55,16 @@ namespace SourceGit.Commands
start.Environment.Add("DISPLAY", "required");
start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec`
start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer");
start.Environment.Add("SOURCEGIT_LAUNCH_AS_ASKPASS", "TRUE");
// If an SSH private key was provided, sets the environment.
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
if (!string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new -i '{SSHKey}'");
else
start.Arguments += "-c credential.helper=manager ";
// 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)
@ -189,31 +87,140 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(WorkingDirectory))
start.WorkingDirectory = WorkingDirectory;
return start;
}
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
private void HandleOutput(string line, List<string> errs)
{
line ??= string.Empty;
Log?.AppendLine(line);
// Lines to hide in error message.
if (line.Length > 0)
proc.OutputDataReceived += (_, e) =>
{
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))
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (REG_PROGRESS().IsMatch(line))
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);
};
try
{
proc.Start();
}
catch (Exception e)
{
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, e.Message);
});
}
return false;
}
errs.Add(line);
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
proc.WaitForExit();
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0 && errs.Count > 0)
{
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, string.Join("\n", errs));
});
}
return false;
}
else
{
return true;
}
}
public ReadToEndResult ReadToEnd()
{
var start = new ProcessStartInfo();
start.FileName = Native.OS.GitExecutable;
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
if (!string.IsNullOrEmpty(WorkingDirectory))
start.WorkingDirectory = WorkingDirectory;
var proc = new Process() { StartInfo = start };
try
{
proc.Start();
}
catch
{
return new ReadToEndResult()
{
IsSuccess = false,
StdOut = string.Empty,
};
}
var rs = new ReadToEndResult()
{
StdOut = proc.StandardOutput.ReadToEnd(),
};
proc.WaitForExit();
rs.IsSuccess = proc.ExitCode == 0;
proc.Close();
return rs;
}
protected virtual void OnReadline(string line) { }
[GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS();
}

View file

@ -4,36 +4,21 @@ 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 autoStage, 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 (autoStage)
Args += " --all";
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

@ -7,29 +7,23 @@ namespace SourceGit.Commands
{
public Config(string repository)
{
if (string.IsNullOrEmpty(repository))
{
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
else
{
WorkingDirectory = repository;
Context = repository;
_isLocal = true;
}
WorkingDirectory = repository;
Context = repository;
RaiseError = false;
}
public Dictionary<string, string> ListAll()
{
Args = "config -l";
if (string.IsNullOrEmpty(WorkingDirectory))
Args = "config --global -l";
else
Args = "config -l";
var output = ReadToEnd();
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);
@ -53,16 +47,22 @@ namespace SourceGit.Commands
public bool Set(string key, string value, bool allowEmpty = false)
{
var scope = _isLocal ? "--local" : "--global";
if (!allowEmpty && string.IsNullOrWhiteSpace(value))
Args = $"config {scope} --unset {key}";
{
if (string.IsNullOrEmpty(WorkingDirectory))
Args = $"config --global --unset {key}";
else
Args = $"config --unset {key}";
}
else
Args = $"config {scope} {key} \"{value}\"";
{
if (string.IsNullOrWhiteSpace(WorkingDirectory))
Args = $"config --global {key} \"{value}\"";
else
Args = $"config {key} \"{value}\"";
}
return Exec();
}
private bool _isLocal = false;
}
}

View file

@ -1,26 +0,0 @@
using System;
namespace SourceGit.Commands
{
public class CountLocalChangesWithoutUntracked : Command
{
public CountLocalChangesWithoutUntracked(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain";
}
public int Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
return lines.Length;
}
return 0;
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@ -8,15 +8,11 @@ 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/";
public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespace)
public Diff(string repo, Models.DiffOption opt, int unified)
{
_result.TextDiff = new Models.TextDiff()
{
@ -26,50 +22,32 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
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}";
else
Args = $"diff --no-ext-diff --patch --unified={unified} {opt}";
Args = $"diff --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 +60,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 +74,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 +85,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 +130,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 +142,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 +153,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 +164,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 +219,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)
{
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).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,155 @@
namespace SourceGit.Commands
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
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;
AutoFetch.MarkFetched(repo);
}
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;
}
public class AutoFetch
{
public static bool IsEnabled
{
get;
set;
} = false;
public static int Interval
{
get => _interval;
set
{
if (value < 1)
return;
_interval = value;
lock (_lock)
{
foreach (var job in _jobs)
{
job.Value.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(_interval));
}
}
}
}
class Job
{
public Fetch Cmd = null;
public DateTime NextRunTimepoint = DateTime.MinValue;
}
static AutoFetch()
{
Task.Run(() =>
{
while (true)
{
if (!IsEnabled)
{
Thread.Sleep(10000);
continue;
}
var now = DateTime.Now;
var uptodate = new List<Job>();
lock (_lock)
{
foreach (var job in _jobs)
{
if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0)
{
uptodate.Add(job.Value);
}
}
}
foreach (var job in uptodate)
{
job.Cmd.Exec();
job.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval));
}
Thread.Sleep(2000);
}
});
}
public static void AddRepository(string repo)
{
var job = new Job
{
Cmd = new Fetch(repo, "--all", true, false, null) { RaiseError = false },
NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)),
};
lock (_lock)
{
_jobs[repo] = job;
}
}
public static void RemoveRepository(string repo)
{
lock (_lock)
{
_jobs.Remove(repo);
}
}
public static void MarkFetched(string repo)
{
lock (_lock)
{
if (_jobs.TryGetValue(repo, out var value))
{
value.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval));
}
}
}
private static readonly Dictionary<string, Job> _jobs = new Dictionary<string, Job>();
private static readonly object _lock = new object();
private static int _interval = 10;
}
}

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;
Args = "gc --prune=now";
TraitErrorAsOutput = true;
Args = "gc --prune";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

View file

@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Avalonia.Threading;
namespace SourceGit.Commands
{
/// <summary>
/// A C# version of https://github.com/anjerodev/commitollama
/// </summary>
public class GenerateCommitMessage
{
public class GetDiffContent : Command
{
public GetDiffContent(string repo, Models.DiffOption opt)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff --diff-algorithm=minimal {opt}";
}
}
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
{
_service = service;
_repo = repo;
_changes = changes;
_cancelToken = cancelToken;
_onResponse = onResponse;
}
public void Exec()
{
try
{
_onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
var responseBuilder = new StringBuilder();
var summaryBuilder = new StringBuilder();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
return;
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");
}
if (_cancelToken.IsCancellationRequested)
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}");
});
}
catch (Exception e)
{
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
}
}
private Models.OpenAIService _service;
private string _repo;
private List<Models.Change> _changes;
private CancellationToken _cancelToken;
private Action<string> _onResponse;
}
}

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,18 +1,21 @@
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 force, bool track, Action<string> onProgress)
{
_outputHandler = onProgress;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "push --progress --verbose ";
if (withTags)
Args += "--tags ";
if (checkSubmodules)
Args += "--recurse-submodules=check ";
if (track)
Args += "-u ";
if (force)
@ -21,7 +24,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 +34,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

@ -7,77 +7,40 @@ namespace SourceGit.Commands
{
private const string PREFIX_LOCAL = "refs/heads/";
private const string PREFIX_REMOTE = "refs/remotes/";
private const string PREFIX_DETACHED_AT = "(HEAD detached at";
private const string PREFIX_DETACHED_FROM = "(HEAD detached from";
private const string PREFIX_DETACHED = "(HEAD detached at";
public QueryBranches(string repo)
{
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)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:trackshort)\"";
}
public List<Models.Branch> Result(out int localBranchesCount)
public List<Models.Branch> Result()
{
localBranchesCount = 0;
Exec();
var branches = new List<Models.Branch>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return branches;
foreach (var b in _needQueryTrackStatus)
b.TrackStatus = new QueryTrackStatus(WorkingDirectory, b.Name, b.Upstream).Result();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var remoteHeads = new Dictionary<string, string>();
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;
return _branches;
}
private Models.Branch ParseLine(string line)
protected override void OnReadline(string line)
{
var parts = line.Split('\0');
if (parts.Length != 6)
return null;
var parts = line.Split('$');
if (parts.Length != 5)
return;
var branch = new Models.Branch();
var refName = parts[0];
if (refName.EndsWith("/HEAD", StringComparison.Ordinal))
return null;
return;
branch.IsDetachedHead = refName.StartsWith(PREFIX_DETACHED_AT, StringComparison.Ordinal) ||
refName.StartsWith(PREFIX_DETACHED_FROM, StringComparison.Ordinal);
if (refName.StartsWith(PREFIX_DETACHED, StringComparison.Ordinal))
{
branch.IsHead = true;
}
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal))
{
@ -89,7 +52,7 @@ namespace SourceGit.Commands
var name = refName.Substring(PREFIX_REMOTE.Length);
var shortNameIdx = name.IndexOf('/', StringComparison.Ordinal);
if (shortNameIdx < 0)
return null;
return;
branch.Remote = name.Substring(0, shortNameIdx);
branch.Name = name.Substring(branch.Remote.Length + 1);
@ -102,19 +65,19 @@ 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))
_needQueryTrackStatus.Add(branch);
else
branch.TrackStatus = new Models.BranchTrackStatus();
return branch;
_branches.Add(branch);
}
private readonly List<Models.Branch> _branches = new List<Models.Branch>();
private List<Models.Branch> _needQueryTrackStatus = new List<Models.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,49 +10,33 @@ 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;
}
public QueryCommits(string repo, string filter, Models.CommitSearchMethod method, bool onlyCurrentBranch)
public QueryCommits(string repo, int maxCount, string messageFilter, bool isFile)
{
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
if (method == Models.CommitSearchMethod.ByAuthor)
string search;
if (isFile)
{
search += $"-i --author=\"{filter}\"";
search = $"-- \"{messageFilter}\"";
}
else if (method == Models.CommitSearchMethod.ByCommitter)
{
search += $"-i --committer=\"{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 = messageFilter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
argsBuilder.Append($"--grep=\"{escaped}\" ");
}
argsBuilder.Append("--all-match -i");
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 -{maxCount} --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 --branches --remotes " + search;
_findFirstMerged = false;
}
@ -120,7 +104,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 +120,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,146 @@ 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;
if (line.EndsWith("/", StringComparison.Ordinal))
return; // Ignore changes with git-worktree
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

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryRefsContainsCommit : Command
{
public QueryRefsContainsCommit(string repo, string commit)
{
WorkingDirectory = repo;
RaiseError = false;
Args = $"for-each-ref --format=\"%(refname)\" --contains {commit}";
}
public List<Models.Decorator> Result()
{
var rs = new List<Models.Decorator>();
var output = ReadToEnd();
if (!output.IsSuccess)
return rs;
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.EndsWith("/HEAD", StringComparison.Ordinal))
continue;
if (line.StartsWith("refs/heads/", StringComparison.Ordinal))
rs.Add(new() { Name = line.Substring("refs/heads/".Length), Type = Models.DecoratorType.LocalBranchHead });
else if (line.StartsWith("refs/remotes/", StringComparison.Ordinal))
rs.Add(new() { Name = line.Substring("refs/remotes/".Length), Type = Models.DecoratorType.RemoteBranchHead });
else if (line.StartsWith("refs/tags/", StringComparison.Ordinal))
rs.Add(new() { Name = line.Substring("refs/tags/".Length), Type = Models.DecoratorType.Tag });
}
return rs;
}
}
}

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

@ -6,6 +6,15 @@
{
WorkingDirectory = path;
Args = "rev-parse --show-toplevel";
RaiseError = false;
}
public string Result()
{
var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs))
return null;
return rs.Trim();
}
}
}

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
@ -8,7 +9,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,18 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QuerySubmodules : Command
{
[GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")]
private static partial Regex REG_FORMAT_STATUS();
[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();
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
private static partial Regex REG_FORMAT2();
public QuerySubmodules(string repo)
{
@ -21,122 +17,28 @@ namespace SourceGit.Commands
Args = "submodule status";
}
public List<Models.Submodule> Result()
public List<string> Result()
{
var submodules = new List<Models.Submodule>();
var rs = ReadToEnd();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var map = new Dictionary<string, Models.Submodule>();
var needCheckLocalChanges = false;
foreach (var line in lines)
{
var match = REG_FORMAT_STATUS().Match(line);
if (match.Success)
{
var stat = match.Groups[1].Value;
var sha = match.Groups[2].Value;
var path = match.Groups[3].Value;
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);
}
}
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}";
rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;
lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_DIRTY().Match(line);
if (match.Success)
{
var path = match.Groups[1].Value;
if (map.TryGetValue(path, out var m))
m.Status = Models.SubmoduleStatus.Modified;
}
}
}
return submodules;
Exec();
return _submodules;
}
private class ModuleInfo
protected override void OnReadline(string line)
{
public string Path { get; set; } = string.Empty;
public string URL { get; set; } = string.Empty;
var match = REG_FORMAT1().Match(line);
if (match.Success)
{
_submodules.Add(match.Groups[1].Value);
return;
}
match = REG_FORMAT2().Match(line);
if (match.Success)
{
_submodules.Add(match.Groups[1].Value);
}
}
private readonly List<string> _submodules = new List<string>();
}
}

View file

@ -7,45 +7,38 @@ namespace SourceGit.Commands
{
public QueryTags(string repo)
{
_boundary = $"----- BOUNDARY OF TAGS {Guid.NewGuid()} -----";
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=\"$%(refname)$%(objectname)$%(*objectname)\"";
}
public List<Models.Tag> Result()
{
var tags = new List<Models.Tag>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return tags;
var records = rs.StdOut.Split(_boundary, StringSplitOptions.RemoveEmptyEntries);
foreach (var record in records)
{
var subs = record.Split('\0', StringSplitOptions.None);
if (subs.Length != 6)
continue;
var name = subs[0].Substring(10);
var message = subs[5].Trim();
if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal))
message = null;
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,
});
}
return tags;
Exec();
return _loaded;
}
private string _boundary = string.Empty;
protected override void OnReadline(string line)
{
var subs = line.Split('$', StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 2)
{
_loaded.Add(new Models.Tag()
{
Name = subs[0].Substring(10),
SHA = subs[1],
});
}
else if (subs.Length == 3)
{
_loaded.Add(new Models.Tag()
{
Name = subs[0].Substring(10),
SHA = subs[2],
});
}
}
private readonly List<Models.Tag> _loaded = new List<Models.Tag>();
}
}

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] == '>')

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