diff --git a/.editorconfig b/.editorconfig index 56725e7b..dedc5722 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes index bd1dfea9..69139978 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 12792cf6..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -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/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50e02dc9..c8e1ea8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,29 +1,42 @@ 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 }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux-x64 + - os: windows-latest + platform: win-x64 + - os: macos-latest + platform: osx-x64 + - os: macos-latest + platform: osx-arm64 + runs-on: ${{ matrix.os }} 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 ${{ matrix.platform }} -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.platform }} + path: publish diff --git a/.github/workflows/localization-check.yml b/.github/workflows/localization-check.yml deleted file mode 100644 index 8dcd61c8..00000000 --- a/.github/workflows/localization-check.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml deleted file mode 100644 index 2dfc97fd..00000000 --- a/.github/workflows/package.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e61e608b..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -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/* diff --git a/.gitignore b/.gitignore index e686a534..05b24d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -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,26 +428,182 @@ .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 -build/resources/ -build/SourceGit/ -build/SourceGit.app/ -build/*.zip -build/*.tar.gz -build/*.deb -build/*.rpm -build/*.AppImage -SourceGit.app/ -build.command -src/Properties/launchSettings.json +# 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 folders. +[Bb]uild/[Ss]ource[Gg]it/ +[Bb]uild/[Ss]ource[Gg]it.app/ + +# Files +SourceGit.win-x64.zip +SourceGit.linux-x64.tar.gz +SourceGit.osx-x64.zip +SourceGit.osx-arm64.zip \ No newline at end of file diff --git a/LICENSE b/LICENSE index 442ce085..dceab2d8 100644 --- a/LICENSE +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index f9ba3072..e84afc3b 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,86 @@ -# SourceGit - Opensource Git GUI client. +# SourceGit -[](https://github.com/sourcegit-scm/sourcegit/stargazers) -[](https://github.com/sourcegit-scm/sourcegit/forks) -[](LICENSE) -[](https://github.com/sourcegit-scm/sourcegit/releases/latest) -[](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/简体中文 * 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 * Branches * Remotes * Tags * Stashes * Submodules - * Worktrees * Archive * Diff * Save as patch/apply * File histories * Blame * 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 -> [!WARNING] -> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. +> **Linux** only tested on **Ubuntu 22.04** on **X11**. -## Translation Status +## How to use -You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md) +**To use this tool, you need to install Git first.** -## How to Use - -**To use this tool, you need to install Git(>=2.25.1) 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. - -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. +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. For **Windows** users: * **MSYS Git is NOT supported**. Please use official [Git for Windows](https://git-scm.com/download/win) instead. -* You can install the latest stable from `winget` with follow commands: - ```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: - ```shell - scoop bucket add extras - scoop install sourcegit - ``` -* Pre-built binaries can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) +* `Source.win-x64.zip` may be reported as virus by Windows Defender. I don't know why. I have manually tested the zip to be uploaded using Windows Defender before uploading and no virus was found. If you have installed .NET 8 SDK locally, I suggest you to compile it yourself. And if you have any idea about how to fix this, please open an issue. 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.osx-x64.zip` or `SourceGit.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. -* 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 +* `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, and it requires `ttf-mscorefonts-installer` installed. +* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI. +* Modify `SourceGit.desktop.template` (replace SOURCEGIT_LOCAL_FOLDER with real path) and move it into `~/.local/share/applications`. ## 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 | +| 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. ## Screenshots * Dark Theme -  + * Light Theme -  - -* Custom - - You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git). And welcome to share your own themes. + ## 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. -[](https://github.com/sourcegit-scm/sourcegit/graphs/contributors) - -## Third-Party Components - -For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md). + + + diff --git a/SourceGit.sln b/SourceGit.sln index 624322f8..1efe00f1 100644 --- a/SourceGit.sln +++ b/SourceGit.sln @@ -6,18 +6,24 @@ 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}" + ProjectSection(SolutionItems) = preProject + build\resources\App.icns = build\resources\App.icns + build\resources\App.plist = build\resources\App.plist + build\resources\SourceGit.desktop.template = build\resources\SourceGit.desktop.template + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F45A9D95-AF25-42D8-BBAC-8259C9EEE820}" 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}" @@ -30,59 +36,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "files", "files", "{3AB707DB global.json = global.json LICENSE = LICENSE README.md = README.md - VERSION = VERSION - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "app", "app", "{ABC98884-F023-4EF4-A9C9-5DE9452BE955}" - ProjectSection(SolutionItems) = preProject - build\resources\app\App.icns = build\resources\app\App.icns - build\resources\app\App.plist = build\resources\app\App.plist - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_common", "_common", "{04FD74B1-FBDB-496E-A48F-3D59D71FF952}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "usr", "usr", "{76639799-54BC-45E8-BD90-F45F63ACD11D}" -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}" - ProjectSection(SolutionItems) = preProject - build\resources\_common\usr\share\applications\sourcegit.desktop = build\resources\_common\usr\share\applications\sourcegit.desktop - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "icons", "icons", "{7166EC6C-17F5-4B5E-B38E-1E53C81EACF6}" - ProjectSection(SolutionItems) = preProject - build\resources\_common\usr\share\icons\sourcegit.png = build\resources\_common\usr\share\icons\sourcegit.png - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deb", "deb", "{9C2F0CDA-B56E-44A5-94B6-F3EA7AC20CDC}" -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}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SPECS", "SPECS", "{7802CD7A-591B-4EDD-96F8-9BF3F61692E4}" - ProjectSection(SolutionItems) = preProject - build\resources\rpm\SPECS\build.spec = build\resources\rpm\SPECS\build.spec - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "appimage", "appimage", "{5D125DD9-B48A-491F-B2FB-D7830D74C4DC}" - ProjectSection(SolutionItems) = preProject - 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 @@ -103,18 +56,6 @@ Global {2091C34D-4A17-4375-BEF3-4D60BE8113E4} = {49A7C2D6-558C-4FAA-8F5D-EEE81497AED7} {FD384607-ED99-47B7-AF31-FB245841BC92} = {773082AC-D9C8-4186-8521-4B6A7BEE6158} {67B6D05F-A000-40BA-ADB4-C9065F880D7B} = {F45A9D95-AF25-42D8-BBAC-8259C9EEE820} - {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} - {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} - {9C2F0CDA-B56E-44A5-94B6-F3EA7AC20CDC} = {FD384607-ED99-47B7-AF31-FB245841BC92} - {F101849D-BDB7-40D4-A516-751150C3CCFC} = {9C2F0CDA-B56E-44A5-94B6-F3EA7AC20CDC} - {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} diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md deleted file mode 100644 index 2338263c..00000000 --- a/THIRD-PARTY-LICENSES.md +++ /dev/null @@ -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 diff --git a/TRANSLATION.md b/TRANSLATION.md deleted file mode 100644 index 051440f0..00000000 --- a/TRANSLATION.md +++ /dev/null @@ -1,511 +0,0 @@ -# Translation Status - -This document shows the translation status of each locale file in the repository. - -## Details - -###  - -###  - - -Missing keys in de_DE.axaml - -- 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 - - - -###  - -###  - - -Missing keys in fr_FR.axaml - -- 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 - - - -###  - - -Missing keys in it_IT.axaml - -- 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 - - - -###  - - -Missing keys in ja_JP.axaml - -- 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 - - - -###  - - -Missing keys in pt_BR.axaml - -- 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 - - - -###  - - -Missing keys in ru_RU.axaml - -- Text.Checkout.WithFastForward -- Text.Checkout.WithFastForward.Upstream - - - -###  - - -Missing keys in ta_IN.axaml - -- 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 - - - -###  - - -Missing keys in uk_UA.axaml - -- 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 - - - -###  - -###  \ No newline at end of file diff --git a/VERSION b/VERSION deleted file mode 100644 index d3e094ba..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2025.22 \ No newline at end of file diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 17305edf..00000000 --- a/build/README.md +++ /dev/null @@ -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. diff --git a/build/build.linux.sh b/build/build.linux.sh new file mode 100644 index 00000000..60db33e8 --- /dev/null +++ b/build/build.linux.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +rm -rf SourceGit +dotnet publish ../src/SourceGit.csproj -c Release -r linux-x64 -o SourceGit -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained +cp resources/SourceGit.desktop.template SourceGit/SourceGit.desktop.template +cp resources/App.icns SourceGit/SourceGit.icns +tar -zcvf SourceGit.linux-x64.tar.gz --exclude="*/*.dbg" SourceGit +rm -rf SourceGit diff --git a/build/build.osx.command b/build/build.osx.command new file mode 100644 index 00000000..2d4127c1 --- /dev/null +++ b/build/build.osx.command @@ -0,0 +1,17 @@ +#!/bin/sh + +rm -rf SourceGit.app + +mkdir -p SourceGit.app/Contents/Resources +cp resources/App.plist SourceGit.app/Contents/Info.plist +cp resources/App.icns SourceGit.app/Contents/Resources/App.icns + +mkdir -p SourceGit.app/Contents/MacOS +dotnet publish ../src/SourceGit.csproj -c Release -r osx-arm64 -o SourceGit.app/Contents/MacOS -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained +zip SourceGit.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 -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained +zip SourceGit.osx-x64.zip -r SourceGit.app -x "*/*\.dsym/*" diff --git a/build/build.windows.ps1 b/build/build.windows.ps1 new file mode 100644 index 00000000..e18e58e8 --- /dev/null +++ b/build/build.windows.ps1 @@ -0,0 +1,13 @@ +if (Test-Path SourceGit) { + Remove-Item SourceGit -Recurse -Force +} + +if (Test-Path SourceGit.win-x64.zip) { + Remove-Item SourceGit.win-x64.zip -Force +} + +dotnet publish ..\src\SourceGit.csproj -c Release -r win-x64 -o SourceGit -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained + +Remove-Item SourceGit\*.pdb -Force + +Compress-Archive -Path SourceGit -DestinationPath SourceGit.win-x64.zip diff --git a/build/resources/app/App.icns b/build/resources/App.icns similarity index 100% rename from build/resources/app/App.icns rename to build/resources/App.icns diff --git a/build/resources/App.plist b/build/resources/App.plist new file mode 100644 index 00000000..3327c5d8 --- /dev/null +++ b/build/resources/App.plist @@ -0,0 +1,26 @@ + + + + + CFBundleIconFile + App.icns + CFBundleIdentifier + com.sourcegit-scm.sourcegit + CFBundleName + SourceGit + CFBundleVersion + 8.8.0 + LSMinimumSystemVersion + 10.12 + CFBundleExecutable + SourceGit + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleShortVersionString + 8.8 + NSHighResolutionCapable + + + diff --git a/build/resources/SourceGit.desktop.template b/build/resources/SourceGit.desktop.template new file mode 100644 index 00000000..ec4b7c41 --- /dev/null +++ b/build/resources/SourceGit.desktop.template @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=SourceGit +Comment=Free & OpenSource Git Client +Exec=SOURCEGIT_LOCAL_FOLDER/SourceGit +Icon=SOURCEGIT_LOCAL_FOLDER/SourceGit.icns +Type=Application +Terminal=false \ No newline at end of file diff --git a/build/resources/_common/applications/sourcegit.desktop b/build/resources/_common/applications/sourcegit.desktop deleted file mode 100644 index bcf9c813..00000000 --- a/build/resources/_common/applications/sourcegit.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=SourceGit -Comment=Open-source & Free Git GUI Client -Exec=/opt/sourcegit/sourcegit -Icon=/usr/share/icons/sourcegit.png -Terminal=false -Type=Application -Categories=Development -MimeType=inode/directory; diff --git a/build/resources/_common/icons/sourcegit.png b/build/resources/_common/icons/sourcegit.png deleted file mode 100644 index 8cdcd3a8..00000000 Binary files a/build/resources/_common/icons/sourcegit.png and /dev/null differ diff --git a/build/resources/app/App.plist b/build/resources/app/App.plist deleted file mode 100644 index ba6f40a2..00000000 --- a/build/resources/app/App.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleIconFile - App.icns - CFBundleIdentifier - com.sourcegit-scm.sourcegit - CFBundleName - SourceGit - CFBundleVersion - SOURCE_GIT_VERSION.0 - LSMinimumSystemVersion - 11.0 - CFBundleExecutable - SourceGit - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleShortVersionString - SOURCE_GIT_VERSION - NSHighResolutionCapable - - - diff --git a/build/resources/appimage/sourcegit.appdata.xml b/build/resources/appimage/sourcegit.appdata.xml deleted file mode 100644 index 012c82d3..00000000 --- a/build/resources/appimage/sourcegit.appdata.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - com.sourcegit_scm.SourceGit - MIT - MIT - SourceGit - Open-source GUI client for git users - - Open-source GUI client for git users - - https://github.com/sourcegit-scm/sourcegit - com.sourcegit_scm.SourceGit.desktop - - com.sourcegit_scm.SourceGit.desktop - - diff --git a/build/resources/appimage/sourcegit.png b/build/resources/appimage/sourcegit.png deleted file mode 100644 index 8cdcd3a8..00000000 Binary files a/build/resources/appimage/sourcegit.png and /dev/null differ diff --git a/build/resources/deb/DEBIAN/control b/build/resources/deb/DEBIAN/control deleted file mode 100755 index 71786b43..00000000 --- a/build/resources/deb/DEBIAN/control +++ /dev/null @@ -1,8 +0,0 @@ -Package: sourcegit -Version: 2025.10 -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 -Architecture: amd64 -Installed-Size: 60440 -Maintainer: longshuang@msn.cn -Description: Open-source & Free Git GUI Client diff --git a/build/resources/deb/DEBIAN/preinst b/build/resources/deb/DEBIAN/preinst deleted file mode 100755 index a93f8090..00000000 --- a/build/resources/deb/DEBIAN/preinst +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -set -e - -# summary of how this script can be called: -# * `install' -# * `install' -# * `upgrade' -# * `abort-upgrade' -# 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 diff --git a/build/resources/deb/DEBIAN/prerm b/build/resources/deb/DEBIAN/prerm deleted file mode 100755 index c2c9e4f0..00000000 --- a/build/resources/deb/DEBIAN/prerm +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh - -set -e - -# summary of how this script can be called: -# * `remove' -# * `upgrade' -# * `failed-upgrade' -# * `remove' `in-favour' -# * `deconfigure' `in-favour' -# `removing' -# -# 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 diff --git a/build/resources/rpm/SPECS/build.spec b/build/resources/rpm/SPECS/build.spec deleted file mode 100644 index 2a684837..00000000 --- a/build/resources/rpm/SPECS/build.spec +++ /dev/null @@ -1,38 +0,0 @@ -Name: sourcegit -Version: %_version -Release: 1 -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 - -%define _build_id_links none - -%description -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 - -%files -%dir /opt/sourcegit/ -/opt/sourcegit/* -/usr/share/applications/sourcegit.desktop -/usr/share/icons/* -%{_bindir}/sourcegit - -%changelog -# skip diff --git a/build/scripts/localization-check.js b/build/scripts/localization-check.js deleted file mode 100644 index 8d636b5b..00000000 --- a/build/scripts/localization-check.js +++ /dev/null @@ -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(`### `); - - 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(' 0) { - const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100; - const badgeColor = progress >= 75 ? 'yellow' : 'red'; - - lines.push(`### }%25-${badgeColor})`); - lines.push(`\nMissing keys in ${file}\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n`) - } else { - lines.push(`### `); - } - } - - const content = lines.join('\n\n'); - console.log(content); - await fs.writeFile(outputFile, content, 'utf8'); -} - -calculateTranslationRate().catch(err => console.error(err)); diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh deleted file mode 100755 index 1b4adbdc..00000000 --- a/build/scripts/package.linux.sh +++ /dev/null @@ -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" ./ diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh deleted file mode 100755 index 2d43e24a..00000000 --- a/build/scripts/package.osx-app.sh +++ /dev/null @@ -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 diff --git a/build/scripts/package.windows.sh b/build/scripts/package.windows.sh deleted file mode 100755 index c22a9d35..00000000 --- a/build/scripts/package.windows.sh +++ /dev/null @@ -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 diff --git a/global.json b/global.json index a27a2b82..b5b37b60 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.0", + "version": "8.0.0", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/screenshots/theme_dark.png b/screenshots/theme_dark.png index 85e18481..4a9646dc 100644 Binary files a/screenshots/theme_dark.png and b/screenshots/theme_dark.png differ diff --git a/screenshots/theme_light.png b/screenshots/theme_light.png index 2e8cf6fc..669b4d18 100644 Binary files a/screenshots/theme_light.png and b/screenshots/theme_light.png differ diff --git a/src/App.Commands.cs b/src/App.Commands.cs deleted file mode 100644 index 22e9fb51..00000000 --- a/src/App.Commands.cs +++ /dev/null @@ -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 action) - { - _action = action; - } - - public bool CanExecute(object parameter) => _action != null; - public void Execute(object parameter) => _action?.Invoke(parameter); - - private Action _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); - }); - } -} diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 9cad0792..af73a68e 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -1,54 +1,9 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -using Avalonia.Controls; -using Avalonia.Media; +using System.Text.Json.Serialization; namespace SourceGit { - public class ColorConverter : JsonConverter - { - public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return Color.Parse(reader.GetString()); - } - - public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString()); - } - } - - public class GridLengthConverter : JsonConverter - { - public override GridLength Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var size = reader.GetDouble(); - return new GridLength(size, GridUnitType.Pixel); - } - - public override void Write(Utf8JsonWriter writer, GridLength value, JsonSerializerOptions options) - { - writer.WriteNumberValue(value.Value); - } - } - - [JsonSourceGenerationOptions( - WriteIndented = true, - IgnoreReadOnlyFields = true, - IgnoreReadOnlyProperties = true, - Converters = [ - typeof(ColorConverter), - typeof(GridLengthConverter), - ] - )] - [JsonSerializable(typeof(Models.ExternalToolPaths))] - [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] - [JsonSerializable(typeof(Models.JetBrainsState))] - [JsonSerializable(typeof(Models.ThemeOverrides))] + [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] [JsonSerializable(typeof(Models.Version))] - [JsonSerializable(typeof(Models.RepositorySettings))] - [JsonSerializable(typeof(ViewModels.Preferences))] + [JsonSerializable(typeof(ViewModels.Preference))] internal partial class JsonCodeGen : JsonSerializerContext { } } diff --git a/src/App.axaml b/src/App.axaml index 186022d5..768ff267 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -1,6 +1,5 @@ @@ -11,37 +10,15 @@ - - - - - - - - - - + - - - - - - - - - - - - - diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 8e579373..8ef9e336 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,13 +1,9 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using Avalonia; @@ -17,7 +13,6 @@ 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; @@ -25,34 +20,33 @@ namespace SourceGit { 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); - }; - - TaskScheduler.UnobservedTaskException += (_, e) => - { - e.SetObserved(); - }; - try { - if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo)) - Environment.Exit(exitTodo); - else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage)) - Environment.Exit(exitMessage); - else - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception ex) { - LogException(ex); + var builder = new StringBuilder(); + builder.Append("Crash: "); + builder.Append(ex.Message); + builder.Append("\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); + + var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + var file = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "SourceGit", + $"crash_{time}.log"); + File.WriteAllText(file, builder.ToString()); } } @@ -61,11 +55,6 @@ namespace SourceGit var builder = AppBuilder.Configure(); builder.UsePlatformDetect(); builder.LogToTrace(); - builder.WithInterFont(); - builder.With(new FontManagerOptions() - { - DefaultFamilyName = "fonts:Inter#Inter" - }); builder.ConfigureFonts(manager => { var monospace = new EmbeddedFontCollection( @@ -78,241 +67,74 @@ namespace SourceGit return builder; } - public 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}\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 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) - { - 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)) - return; - - var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views."); - var viewType = Type.GetType(viewTypeName); - if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow))) - return; - - window = Activator.CreateInstance(viewType) as Views.ChromelessWindow; - if (window != null) - { - window.DataContext = data; - impl(window, showAsDialog); - } - } - public static void RaiseException(string context, string message) { - if (Current is App app && app._launcher != null) - app._launcher.DispatchNotification(context, message, true); + if (Current is App app && app._notificationReceiver != null) + { + var notice = new Models.Notification() { IsError = true, Message = message }; + app._notificationReceiver.OnReceiveNotification(context, notice); + } } public static void SendNotification(string context, string message) { - if (Current is App app && app._launcher != null) - app._launcher.DispatchNotification(context, message, false); + if (Current is App app && app._notificationReceiver != null) + { + var notice = new Models.Notification() { IsError = false, Message = message }; + app._notificationReceiver.OnReceiveNotification(context, notice); + } } public static void SetLocale(string localeKey) { var app = Current as App; - if (app == null) - return; - var targetLocale = app.Resources[localeKey] as ResourceDictionary; if (targetLocale == null || targetLocale == app._activeLocale) + { return; + } if (app._activeLocale != null) + { app.Resources.MergedDictionaries.Remove(app._activeLocale); + } app.Resources.MergedDictionaries.Add(targetLocale); app._activeLocale = targetLocale; } - public static void SetTheme(string theme, string themeOverridesFile) + public static void SetTheme(string theme) { - var app = Current as App; - if (app == null) - return; - if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) - app.RequestedThemeVariant = ThemeVariant.Light; + { + Current.RequestedThemeVariant = ThemeVariant.Light; + } else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) - app.RequestedThemeVariant = ThemeVariant.Dark; - else - app.RequestedThemeVariant = ThemeVariant.Default; - - if (app._themeOverrides != null) { - app.Resources.MergedDictionaries.Remove(app._themeOverrides); - app._themeOverrides = null; - } - - if (!string.IsNullOrEmpty(themeOverridesFile) && File.Exists(themeOverridesFile)) - { - try - { - var resDic = new ResourceDictionary(); - var overrides = JsonSerializer.Deserialize(File.ReadAllText(themeOverridesFile), JsonCodeGen.Default.ThemeOverrides); - foreach (var kv in overrides.BasicColors) - { - if (kv.Key.Equals("SystemAccentColor", StringComparison.Ordinal)) - resDic["SystemAccentColor"] = kv.Value; - else - resDic[$"Color.{kv.Key}"] = kv.Value; - } - - if (overrides.GraphColors.Count > 0) - Models.CommitGraph.SetPens(overrides.GraphColors, overrides.GraphPenThickness); - else - Models.CommitGraph.SetDefaultPens(overrides.GraphPenThickness); - - Models.Commit.OpacityForNotMerged = overrides.OpacityForNotMergedCommits; - - app.Resources.MergedDictionaries.Add(resDic); - app._themeOverrides = resDic; - } - catch - { - // ignore - } + Current.RequestedThemeVariant = ThemeVariant.Dark; } else { - Models.CommitGraph.SetDefaultPens(); - } - } - - 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; + Current.RequestedThemeVariant = ThemeVariant.Default; } } public static async void CopyText(string data) { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - if (desktop.MainWindow?.Clipboard is { } clipboard) - await clipboard.SetTextAsync(data ?? ""); - } - } - - public static async Task GetClipboardTextAsync() - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - if (desktop.MainWindow?.Clipboard is { } clipboard) + if (desktop.MainWindow.Clipboard is { } clipbord) { - return await clipboard.GetTextAsync(); + await clipbord.SetTextAsync(data); } } - return null; } public static string Text(string key, params object[] args) { - var fmt = Current?.FindResource($"Text.{key}") as string; + var fmt = Current.FindResource($"Text.{key}") as string; if (string.IsNullOrWhiteSpace(fmt)) return $"Text.{key}"; - - if (args == null || args.Length == 0) - return fmt; - return string.Format(fmt, args); } @@ -322,300 +144,30 @@ namespace SourceGit icon.Width = 12; icon.Height = 12; icon.Stretch = Stretch.Uniform; - - if (Current?.FindResource(key) is StreamGeometry geo) - icon.Data = geo; - + icon.Data = Current.FindResource(key) as StreamGeometry; return icon; } - public static IStorageProvider GetStorageProvider() + public static TopLevel GetTopLevel() { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - return desktop.MainWindow?.StorageProvider; - + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + return desktop.MainWindow; + } return null; } - public static ViewModels.Launcher GetLauncher() - { - return Current is App app ? app._launcher : null; - } - - public static void Quit(int exitCode) - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - desktop.MainWindow?.Close(); - desktop.Shutdown(exitCode); - } - else - { - Environment.Exit(exitCode); - } - } - #endregion - - #region Overrides - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - - var pref = ViewModels.Preferences.Instance; - pref.PropertyChanged += (_, _) => pref.Save(); - - SetLocale(pref.Locale); - SetTheme(pref.Theme, pref.ThemeOverrides); - SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - BindingPlugins.DataValidators.RemoveAt(0); - - // Disable tooltip if window is not active. - ToolTip.ToolTipOpeningEvent.AddClassHandler((c, e) => - { - var topLevel = TopLevel.GetTopLevel(c); - if (topLevel is not Window { IsActive: true }) - e.Cancel = true; - }); - - if (TryLaunchAsCoreEditor(desktop)) - return; - - if (TryLaunchAsAskpass(desktop)) - return; - - _ipcChannel = new Models.IpcChannel(); - if (!_ipcChannel.IsFirstInstance) - { - var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty; - if (!string.IsNullOrEmpty(arg)) - { - 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); - } - } - } - #endregion - - private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) - { - exitCode = -1; - - if (args.Length <= 1 || !args[0].Equals("--rebase-todo-editor", StringComparison.Ordinal)) - return false; - - var file = args[1]; - var filename = Path.GetFileName(file); - if (!filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) - return true; - - var dirInfo = new DirectoryInfo(Path.GetDirectoryName(file)!); - if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) - return true; - - var jobsFile = Path.Combine(dirInfo.Parent!.FullName, "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) - return true; - - var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); - var lines = new List(); - foreach (var job in collection.Jobs) - { - switch (job.Action) - { - case Models.InteractiveRebaseAction.Pick: - lines.Add($"p {job.SHA}"); - break; - case Models.InteractiveRebaseAction.Edit: - lines.Add($"e {job.SHA}"); - break; - case Models.InteractiveRebaseAction.Reword: - lines.Add($"r {job.SHA}"); - break; - case Models.InteractiveRebaseAction.Squash: - lines.Add($"s {job.SHA}"); - break; - case Models.InteractiveRebaseAction.Fixup: - lines.Add($"f {job.SHA}"); - break; - default: - lines.Add($"d {job.SHA}"); - break; - } - } - - File.WriteAllLines(file, lines); - - exitCode = 0; - return true; - } - - private static bool TryLaunchAsRebaseMessageEditor(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)) - 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)) - return true; - - var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - if (done.Length == 0) - 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; - } - } - - return true; - } - - private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) - { - var args = desktop.Args; - if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal)) - return false; - - var file = args[1]; - if (!File.Exists(file)) - { - desktop.Shutdown(-1); - return true; - } - - var editor = new Views.StandaloneCommitMessageEditor(); - editor.SetFile(file); - desktop.MainWindow = editor; - return true; - } - - private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) - { - var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS"); - if (launchAsAskpass is not "TRUE") - 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; - } - - return false; - } - - private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) - { - Native.OS.SetupExternalTools(); - Models.AvatarManager.Instance.Start(); - - string startupRepo = null; - if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) - startupRepo = desktop.Args[0]; - - var pref = ViewModels.Preferences.Instance; - pref.SetCanModify(); - - _launcher = new ViewModels.Launcher(startupRepo); - desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; - desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; - -#if !DISABLE_UPDATE_DETECTION - if (pref.ShouldCheck4UpdateOnStartup()) - Check4Update(); -#endif - } - - private void TryOpenRepository(string repo) - { - if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) - { - var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd(); - if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) - { - Dispatcher.UIThread.Invoke(() => - { - var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); - ViewModels.Welcome.Instance.Refresh(); - _launcher?.OpenRepositoryInTab(node, null); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd }) - wnd.BringToTop(); - }); - - return; - } - } - - Dispatcher.UIThread.Invoke(() => - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher }) - launcher.BringToTop(); - }); - } - - private void Check4Update(bool manually = false) + public static 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"); + // Fetch lastest release information. + var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) }; + var data = await client.GetStringAsync("https://api.github.com/repos/sourcegit-scm/sourcegit/releases/latest"); - // Parse JSON into Models.Version. + // Parse json into Models.Version. var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version); if (ver == null) return; @@ -631,7 +183,7 @@ namespace SourceGit // Should not check ignored tag if this is called manually. if (!manually) { - var pref = ViewModels.Preferences.Instance; + var pref = ViewModels.Preference.Instance; if (ver.TagName == pref.IgnoreUpdateTag) return; } @@ -641,66 +193,70 @@ namespace SourceGit catch (Exception e) { if (manually) - ShowSelfUpdateResult(new Models.SelfUpdateFailed(e)); + ShowSelfUpdateResult(e); } }); } - private void ShowSelfUpdateResult(object data) + public static void Quit() + { + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow.Close(); + desktop.Shutdown(); + } + } + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + var pref = ViewModels.Preference.Instance; + + SetLocale(pref.Locale); + SetTheme(pref.Theme); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + BindingPlugins.DataValidators.RemoveAt(0); + + var launcher = new Views.Launcher(); + _notificationReceiver = launcher; + desktop.MainWindow = launcher; + + if (ViewModels.Preference.Instance.ShouldCheck4UpdateOnStartup) + { + ViewModels.Preference.Save(); + Check4Update(); + } + } + + base.OnFrameworkInitializationCompleted(); + } + + private static void ShowSelfUpdateResult(object data) { Dispatcher.UIThread.Post(() => { - ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true); + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var dialog = new Views.SelfUpdate() + { + DataContext = new ViewModels.SelfUpdate + { + Data = data + } + }; + + dialog.Show(desktop.MainWindow); + } }); } - private string FixFontFamilyName(string input) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - var parts = input.Split(','); - var trimmed = new List(); - - 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; + private Models.INotificationReceiver _notificationReceiver = null; } } diff --git a/src/App.manifest b/src/App.manifest index 11a2ff11..b3bc3bdf 100644 --- a/src/App.manifest +++ b/src/App.manifest @@ -1,7 +1,7 @@ diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs index 210eb4b2..2251c892 100644 --- a/src/Commands/Add.cs +++ b/src/Commands/Add.cs @@ -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 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(); + } } } } diff --git a/src/Commands/Archive.cs b/src/Commands/Archive.cs index 5e0919f7..d4f6241c 100644 --- a/src/Commands/Archive.cs +++ b/src/Commands/Archive.cs @@ -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 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 _outputHandler; } } diff --git a/src/Commands/AssumeUnchanged.cs b/src/Commands/AssumeUnchanged.cs index 28f78280..f4cfe32b 100644 --- a/src/Commands/AssumeUnchanged.cs +++ b/src/Commands/AssumeUnchanged.cs @@ -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 + { + partial class ViewCommand : Command + { + [GeneratedRegex(@"^(\w)\s+(.+)$")] + private static partial Regex REG(); + + public ViewCommand(string repo) + { + WorkingDirectory = repo; + Args = "ls-files -v"; + RaiseError = false; + } + + public List Result() + { + Exec(); + return _outs; + } + + protected override void OnReadline(string line) + { + var match = REG().Match(line); + if (!match.Success) + return; + + if (match.Groups[1].Value == "h") + { + _outs.Add(match.Groups[2].Value); + } + } + + private readonly List _outs = new List(); } + + 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 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; } } diff --git a/src/Commands/Bisect.cs b/src/Commands/Bisect.cs deleted file mode 100644 index a3bf1a97..00000000 --- a/src/Commands/Bisect.cs +++ /dev/null @@ -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}"; - } - } -} diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 1fc51fa4..5d047d8c 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -6,8 +6,10 @@ namespace SourceGit.Commands { public partial class Blame : Command { + [GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] private static partial Regex REG_FORMAT(); + private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); public Blame(string repo, string file, string revision) { @@ -21,17 +23,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 +44,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 +67,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 = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd"); var info = new Models.BlameLineInfo() { @@ -89,7 +89,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; diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 0d1b1f8f..21210238 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -1,82 +1,47 @@ -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"; + } else + { cmd.Args = $"branch {name} -u {upstream}"; - + } return cmd.Exec(); } - public static bool DeleteLocal(string repo, string name, Models.ICommandLog log) + public static bool Delete(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) - { - 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; return cmd.Exec(); } } diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index d2876740..d65e9e73 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -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 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 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 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 files) { - var builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(); builder.Append("checkout --ours --"); foreach (var f in files) { @@ -75,8 +58,29 @@ namespace SourceGit.Commands public bool FileWithRevision(string file, string revision) { - Args = $"checkout --no-overlay {revision} -- \"{file}\""; + Args = $"checkout {revision} -- \"{file}\""; return Exec(); } + + public bool Files(List 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 _outputHandler; } } diff --git a/src/Commands/CherryPick.cs b/src/Commands/CherryPick.cs index 0c82b9fd..504769c0 100644 --- a/src/Commands/CherryPick.cs +++ b/src/Commands/CherryPick.cs @@ -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}"; } } } diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs index 6ed74999..900a7f93 100644 --- a/src/Commands/Clean.cs +++ b/src/Commands/Clean.cs @@ -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 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(); } } } diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index efec264b..80e0df50 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -1,21 +1,40 @@ -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 _notifyProgress; + + public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action ouputHandler) { Context = ctx; WorkingDirectory = path; - SSHKey = sshKey; - Args = "clone --progress --verbose "; + TraitErrorAsOutput = true; + + if (string.IsNullOrEmpty(sshKey)) + { + Args = "-c credential.helper=manager "; + } + else + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + + Args += "clone --progress --verbose --recurse-submodules "; if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} "; - Args += $"{url} "; - if (!string.IsNullOrEmpty(localName)) Args += localName; + + _notifyProgress = ouputHandler; + } + + protected override void OnReadline(string line) + { + _notifyProgress?.Invoke(line); } } } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 975922fc..c30a3d45 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -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,66 +10,110 @@ namespace SourceGit.Commands { public partial class Command { - public class ReadToEndResult + public class CancelToken { - public bool IsSuccess { get; set; } = false; - public string StdOut { get; set; } = ""; - public string StdErr { get; set; } = ""; + public bool Requested { get; set; } = false; } - public enum EditorType + public class ReadToEndResult { - None, - CoreEditor, - RebaseEditor, + public bool IsSuccess { get; set; } + public string StdOut { get; set; } + public string StdErr { get; set; } } 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 = 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; + + // Force using en_US.UTF-8 locale to avoid GCM crash + if (OperatingSystem.IsLinux()) + { + start.Environment.Add("LANG", "en_US.UTF-8"); + } + + if (!string.IsNullOrEmpty(WorkingDirectory)) + start.WorkingDirectory = WorkingDirectory; - var start = CreateGitStartInfo(); var errs = new List(); var proc = new Process() { StartInfo = start }; + var isCancelled = false; - proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); - proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs); + proc.OutputDataReceived += (_, e) => + { + if (Cancel != null && Cancel.Requested) + { + isCancelled = true; + proc.CancelErrorRead(); + proc.CancelOutputRead(); + if (!proc.HasExited) + proc.Kill(true); + return; + } + + if (e.Data != null) + OnReadline(e.Data); + }; + + proc.ErrorDataReceived += (_, e) => + { + if (Cancel != null && Cancel.Requested) + { + isCancelled = true; + proc.CancelErrorRead(); + proc.CancelOutputRead(); + if (!proc.HasExited) + proc.Kill(true); + return; + } + + if (string.IsNullOrEmpty(e.Data)) + return; + if (TraitErrorAsOutput) + OnReadline(e.Data); + + // Ignore progress messages + if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal)) + return; + if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) + return; + if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) + return; + if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) + return; + if (_progressRegex().IsMatch(e.Data)) + return; + errs.Add(e.Data); + }; - var dummy = null as Process; - var dummyProcLock = new object(); try { proc.Start(); - - // It not safe, please only use `CancellationToken` in readonly commands. - if (CancellationToken.CanBeCanceled) - { - dummy = proc; - CancellationToken.Register(() => - { - lock (dummyProcLock) - { - if (dummy is { HasExited: false }) - dummy.Kill(); - } - }); - } } catch (Exception e) { if (RaiseError) - Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message)); - - Log?.AppendLine(string.Empty); + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(Context, e.Message); + }); + } return false; } @@ -78,38 +121,42 @@ namespace SourceGit.Commands proc.BeginErrorReadLine(); proc.WaitForExit(); - if (dummy != null) - { - lock (dummyProcLock) - { - dummy = null; - } - } - int exitCode = proc.ExitCode; proc.Close(); - Log?.AppendLine(string.Empty); - if (!CancellationToken.IsCancellationRequested && exitCode != 0) + if (!isCancelled && exitCode != 0 && errs.Count > 0) { if (RaiseError) { - var errMsg = string.Join("\n", errs).Trim(); - if (!string.IsNullOrEmpty(errMsg)) - Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg)); + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(Context, string.Join("\n", errs)); + }); } - return false; } - - return true; + else + { + return true; + } } public ReadToEndResult ReadToEnd() { - var start = CreateGitStartInfo(); - var proc = new Process() { StartInfo = start }; + 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(); @@ -137,84 +184,9 @@ namespace SourceGit.Commands 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.UseShellExecute = false; - start.CreateNoWindow = true; - start.RedirectStandardOutput = true; - start.RedirectStandardError = true; - start.StandardOutputEncoding = Encoding.UTF8; - start.StandardErrorEncoding = Encoding.UTF8; - - // Force using this app as SSH askpass program - var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; - if (!OperatingSystem.IsLinux()) - 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}'"); - - // Force using en_US.UTF-8 locale - if (OperatingSystem.IsLinux()) - { - start.Environment.Add("LANG", "C"); - start.Environment.Add("LC_ALL", "C"); - } - - // Force using this app as git editor. - switch (Editor) - { - case EditorType.CoreEditor: - start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" "; - break; - case EditorType.RebaseEditor: - start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true "; - break; - default: - start.Arguments += "-c core.editor=true "; - break; - } - - // Append command args - start.Arguments += Args; - - // Working directory - if (!string.IsNullOrEmpty(WorkingDirectory)) - start.WorkingDirectory = WorkingDirectory; - - return start; - } - - private void HandleOutput(string line, List errs) - { - line ??= string.Empty; - Log?.AppendLine(line); - - // Lines to hide in error message. - if (line.Length > 0) - { - if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) || - line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) || - line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) || - line.StartsWith("Filtering content:", StringComparison.Ordinal) || - line.StartsWith("hint:", StringComparison.Ordinal)) - return; - - if (REG_PROGRESS().IsMatch(line)) - return; - } - - errs.Add(line); - } + protected virtual void OnReadline(string line) { } [GeneratedRegex(@"\d+%")] - private static partial Regex REG_PROGRESS(); + private static partial Regex _progressRegex(); } } diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index 1585e7e3..8ac6501f 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,36 +4,18 @@ namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor) + public Commit(string repo, string message, bool amend, bool allowEmpty = false) { - _tmpFile = Path.GetTempFileName(); - File.WriteAllText(_tmpFile, message); + var file = Path.GetTempFileName(); + File.WriteAllText(file, message); WorkingDirectory = repo; Context = repo; - Args = $"commit --allow-empty --file=\"{_tmpFile}\""; - if (signOff) - Args += " --signoff"; + Args = $"commit --file=\"{file}\""; if (amend) - Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit"; + Args += " --amend --no-edit"; + if (allowEmpty) + Args += " --allow-empty"; } - - public bool Run() - { - var succ = Exec(); - - try - { - File.Delete(_tmpFile); - } - catch - { - // Ignore - } - - return succ; - } - - private readonly string _tmpFile; } } diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index c88e087a..a9efb36c 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -1,63 +1,32 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.RegularExpressions; 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) { WorkingDirectory = repo; Context = repo; - - var based = string.IsNullOrEmpty(start) ? "-R" : start; - 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}\""; + Args = $"diff --name-status {start} {end}"; } public List 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) => l.Path.CompareTo(r.Path)); 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 +45,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); diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index 49e8fcb7..62340aa3 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -7,17 +7,8 @@ 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; } @@ -29,7 +20,7 @@ namespace SourceGit.Commands var rs = new Dictionary(); if (output.IsSuccess) { - var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var lines = output.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var idx = line.IndexOf('=', StringComparison.Ordinal); @@ -37,7 +28,14 @@ namespace SourceGit.Commands { var key = line.Substring(0, idx).Trim(); var val = line.Substring(idx + 1).Trim(); - rs[key] = val; + if (rs.ContainsKey(key)) + { + rs[key] = val; + } + else + { + rs.Add(key, val); + } } } } @@ -53,16 +51,30 @@ 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; } } diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChangesWithoutUntracked.cs deleted file mode 100644 index a704f313..00000000 --- a/src/Commands/CountLocalChangesWithoutUntracked.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 6af0a3cc..e92b2234 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -8,68 +8,44 @@ 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) { - _result.TextDiff = new Models.TextDiff() - { - Repo = repo, - Option = opt, - }; - 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=4 {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 +58,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 +72,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 +83,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 +128,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 +140,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 +151,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 +162,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; - } } } @@ -271,10 +214,9 @@ namespace SourceGit.Commands } } - private readonly Models.DiffResult _result = new Models.DiffResult(); + private readonly Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() }; private readonly List _deleted = new List(); private readonly List _added = new List(); - private Models.TextDiffLine _last = null; private int _oldLine = 0; private int _newLine = 0; } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index f36ca6c9..072dc59a 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -1,95 +1,55 @@ using System; using System.Collections.Generic; -using System.IO; - -using Avalonia.Threading; namespace SourceGit.Commands { public static class Discard { - /// - /// Discard all local changes (unstaged & staged) - /// - /// - /// - /// - 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 Reset(repo, "HEAD", "--hard").Exec(); + new Clean(repo).Exec(); } - /// - /// Discard selected changes (only unstaged). - /// - /// - /// - /// - public static void Changes(string repo, List changes, Models.ICommandLog log) + public static void ChangesInWorkTree(string repo, List changes) { - var restores = new List(); + var needClean = new List(); + var needCheckout = new List(); - try + foreach (var c in changes) { - foreach (var c in changes) + if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) { - 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); - } + needClean.Add(c.Path); + } + else + { + needCheckout.Add(c.Path); } } - catch (Exception e) + + for (int i = 0; i < needClean.Count; i += 10) { - Dispatcher.UIThread.Invoke(() => - { - App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); - }); + var count = Math.Min(10, needClean.Count - i); + new Clean(repo, needClean.GetRange(i, count)).Exec(); } - if (restores.Count > 0) + for (int i = 0; i < needCheckout.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, needCheckout.Count - i); + new Checkout(repo).Files(needCheckout.GetRange(i, count)); + } + } + + public static void ChangesInStaged(string repo, List changes) + { + for (int i = 0; i < changes.Count; i += 10) + { + var count = Math.Min(10, changes.Count - i); + var files = new List(); + for (int j = 0; j < count; j++) + files.Add(changes[i + j].Path); + new Restore(repo, files, "--staged --worktree").Exec(); } } } diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs deleted file mode 100644 index e59bc068..00000000 --- a/src/Commands/ExecuteCustomAction.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index edf2a6dd..8f84c346 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -1,31 +1,158 @@ -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, Action outputHandler) { + _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - Args = "fetch --progress --verbose "; + TraitErrorAsOutput = true; - if (noTags) - Args += "--no-tags "; + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } else - Args += "--tags "; - - if (force) - Args += "--force "; + { + Args = "-c credential.helper=manager "; + } + Args += "fetch --force --progress --verbose "; + if (prune) + Args += "--prune "; 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 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; + + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + Args = "-c credential.helper=manager "; + } + + Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}"; } + + protected override void OnReadline(string line) + { + _outputHandler?.Invoke(line); + } + + private readonly Action _outputHandler; + } + + public class AutoFetch + { + private const double INTERVAL = 10 * 60; + + public static bool IsEnabled + { + get; + set; + } = false; + + 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(); + 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.AddSeconds(INTERVAL); + } + + Thread.Sleep(2000); + } + }); + } + + public static void AddRepository(string repo) + { + var job = new Job + { + Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false }, + NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL), + }; + + lock (_lock) + { + if (_jobs.ContainsKey(repo)) + { + _jobs[repo] = job; + } + else + { + _jobs.Add(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.AddSeconds(INTERVAL); + } + } + } + + private static readonly Dictionary _jobs = new Dictionary(); + private static readonly object _lock = new object(); } } diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs index bf850d60..2c7359c0 100644 --- a/src/Commands/FormatPatch.cs +++ b/src/Commands/FormatPatch.cs @@ -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}\""; } } } diff --git a/src/Commands/GC.cs b/src/Commands/GC.cs index 0b27f487..f40b665c 100644 --- a/src/Commands/GC.cs +++ b/src/Commands/GC.cs @@ -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 outputHandler) { + _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - Args = "gc --prune=now"; + TraitErrorAsOutput = true; + Args = "gc"; } + + protected override void OnReadline(string line) + { + _outputHandler?.Invoke(line); + } + + private readonly Action _outputHandler; } } diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs deleted file mode 100644 index df61fdd2..00000000 --- a/src/Commands/GenerateCommitMessage.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; - -using Avalonia.Threading; - -namespace SourceGit.Commands -{ - /// - /// A C# version of https://github.com/anjerodev/commitollama - /// - 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 changes, CancellationToken cancelToken, Action 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 _changes; - private CancellationToken _cancelToken; - private Action _onResponse; - } -} diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 1d33fa3a..e47631c1 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -1,92 +1,90 @@ -using System.Text; +using System.Collections.Generic; + using Avalonia.Threading; namespace SourceGit.Commands { - public static class GitFlow + public class GitFlow : Command { - public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log) + public GitFlow(string repo) { - var config = new Config(repo); - config.Set("gitflow.branch.master", master); - config.Set("gitflow.branch.develop", develop); - config.Set("gitflow.prefix.feature", feature); - config.Set("gitflow.prefix.bugfix", "bugfix/"); - config.Set("gitflow.prefix.release", release); - config.Set("gitflow.prefix.hotfix", hotfix); - config.Set("gitflow.prefix.support", "support/"); - config.Set("gitflow.prefix.versiontag", version, true); - - var init = new Command(); - init.WorkingDirectory = repo; - init.Context = repo; - init.Args = "flow init -d"; - init.Log = log; - return init.Exec(); + WorkingDirectory = repo; + Context = repo; } - public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) + public bool Init(List branches, string master, string develop, string feature, string release, string hotfix, string version) { - var start = new Command(); - start.WorkingDirectory = repo; - start.Context = repo; + var current = branches.Find(x => x.IsCurrent); + var masterBranch = branches.Find(x => x.Name == master); + if (masterBranch == null && current != null) + Branch.Create(WorkingDirectory, master, current.Head); + + var devBranch = branches.Find(x => x.Name == develop); + if (devBranch == null && current != null) + Branch.Create(WorkingDirectory, develop, current.Head); + + var cmd = new Config(WorkingDirectory); + cmd.Set("gitflow.branch.master", master); + cmd.Set("gitflow.branch.develop", develop); + cmd.Set("gitflow.prefix.feature", feature); + cmd.Set("gitflow.prefix.bugfix", "bugfix/"); + cmd.Set("gitflow.prefix.release", release); + cmd.Set("gitflow.prefix.hotfix", hotfix); + cmd.Set("gitflow.prefix.support", "support/"); + cmd.Set("gitflow.prefix.versiontag", version, true); + + Args = "flow init -d"; + return Exec(); + } + + public bool Start(Models.GitFlowBranchType type, string name) + { switch (type) { case Models.GitFlowBranchType.Feature: - start.Args = $"flow feature start {name}"; + Args = $"flow feature start {name}"; break; case Models.GitFlowBranchType.Release: - start.Args = $"flow release start {name}"; + Args = $"flow release start {name}"; break; case Models.GitFlowBranchType.Hotfix: - start.Args = $"flow hotfix start {name}"; + Args = $"flow hotfix start {name}"; break; default: - Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(Context, "Bad branch type!!!"); + }); return false; } - start.Log = log; - return start.Exec(); + return Exec(); } - public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) + public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch) { - var builder = new StringBuilder(); - builder.Append("flow "); - + var option = keepBranch ? "-k" : string.Empty; switch (type) { case Models.GitFlowBranchType.Feature: - builder.Append("feature"); + Args = $"flow feature finish {option} {name}"; break; case Models.GitFlowBranchType.Release: - builder.Append("release"); + Args = $"flow release finish {option} {name} -m \"RELEASE_DONE\""; break; case Models.GitFlowBranchType.Hotfix: - builder.Append("hotfix"); + Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\""; break; default: - Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(Context, "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 finish = new Command(); - finish.WorkingDirectory = repo; - finish.Context = repo; - finish.Args = builder.ToString(); - finish.Log = log; - return finish.Exec(); + return Exec(); } } } diff --git a/src/Commands/GitIgnore.cs b/src/Commands/GitIgnore.cs deleted file mode 100644 index 8b351f5e..00000000 --- a/src/Commands/GitIgnore.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.IO; - -namespace SourceGit.Commands -{ - public static class GitIgnore - { - public static void Add(string repo, string pattern) - { - 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]); - } - } -} diff --git a/src/Commands/IsBareRepository.cs b/src/Commands/IsBareRepository.cs deleted file mode 100644 index f92d0888..00000000 --- a/src/Commands/IsBareRepository.cs +++ /dev/null @@ -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"; - } - } -} diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs index af8f54bb..de59b5a4 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -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; } diff --git a/src/Commands/IsCommitSHA.cs b/src/Commands/IsCommitSHA.cs deleted file mode 100644 index 1b0c50e3..00000000 --- a/src/Commands/IsCommitSHA.cs +++ /dev/null @@ -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"); - } - } -} diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs deleted file mode 100644 index 9b243451..00000000 --- a/src/Commands/IsConflictResolved.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SourceGit.Commands -{ - public class IsConflictResolved : Command - { - public IsConflictResolved(string repo, Models.Change change) - { - var opt = new Models.DiffOption(change, true); - - WorkingDirectory = repo; - Context = repo; - Args = $"diff -a --ignore-cr-at-eol --check {opt}"; - } - - public bool Result() - { - return ReadToEnd().IsSuccess; - } - } -} diff --git a/src/Commands/IsLFSFiltered.cs b/src/Commands/IsLFSFiltered.cs index 2a7234bb..b29039de 100644 --- a/src/Commands/IsLFSFiltered.cs +++ b/src/Commands/IsLFSFiltered.cs @@ -6,15 +6,7 @@ { WorkingDirectory = repo; Context = repo; - Args = $"check-attr -z filter \"{path}\""; - RaiseError = false; - } - - public IsLFSFiltered(string repo, string sha, string path) - { - WorkingDirectory = repo; - Context = repo; - Args = $"check-attr --source {sha} -z filter \"{path}\""; + Args = $"check-attr -a -z \"{path}\""; RaiseError = false; } diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs index 18d2ba93..3b8a1cc2 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -1,24 +1,27 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Text.RegularExpressions; namespace SourceGit.Commands { - public partial class LFS + public class LFS { - [GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] - private static partial Regex REG_LOCK(); - - private class SubCmd : Command + class PruneCmd : Command { - public SubCmd(string repo, string args, Models.ICommandLog log) + public PruneCmd(string repo, Action onProgress) { WorkingDirectory = repo; Context = repo; - Args = args; - Log = log; + Args = "lfs prune"; + TraitErrorAsOutput = true; + _outputHandler = onProgress; } + + protected override void OnReadline(string line) + { + _outputHandler?.Invoke(line); + } + + private readonly Action _outputHandler; } public LFS(string repo) @@ -36,78 +39,9 @@ namespace SourceGit.Commands return content.Contains("git lfs pre-push"); } - public bool Install(Models.ICommandLog log) + public void Prune(Action outputHandler) { - return new SubCmd(_repo, "lfs install --local", log).Exec(); - } - - public bool Track(string pattern, bool isFilenameMode, Models.ICommandLog log) - { - var opt = isFilenameMode ? "--filename" : ""; - return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", log).Exec(); - } - - public void Fetch(string remote, Models.ICommandLog log) - { - new SubCmd(_repo, $"lfs fetch {remote}", log).Exec(); - } - - public void Pull(string remote, Models.ICommandLog log) - { - new SubCmd(_repo, $"lfs pull {remote}", log).Exec(); - } - - public void Push(string remote, Models.ICommandLog log) - { - new SubCmd(_repo, $"lfs push {remote}", log).Exec(); - } - - public void Prune(Models.ICommandLog log) - { - new SubCmd(_repo, "lfs prune", log).Exec(); - } - - public List Locks(string remote) - { - var locks = new List(); - var cmd = new SubCmd(_repo, $"lfs locks --remote={remote}", null); - var rs = cmd.ReadToEnd(); - if (rs.IsSuccess) - { - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - var match = REG_LOCK().Match(line); - if (match.Success) - { - locks.Add(new Models.LFSLock() - { - File = match.Groups[1].Value, - User = match.Groups[2].Value, - ID = long.Parse(match.Groups[3].Value), - }); - } - } - } - - return locks; - } - - public bool Lock(string remote, string file, Models.ICommandLog log) - { - return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", log).Exec(); - } - - public bool Unlock(string remote, string file, bool force, Models.ICommandLog log) - { - var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", log).Exec(); - } - - public bool Unlock(string remote, long id, bool force, Models.ICommandLog log) - { - var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", log).Exec(); + new PruneCmd(_repo, outputHandler).Exec(); } private readonly string _repo; diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs index b08377b9..cf2e285f 100644 --- a/src/Commands/Merge.cs +++ b/src/Commands/Merge.cs @@ -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 outputHandler) { + _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; + TraitErrorAsOutput = true; Args = $"merge --progress {source} {mode}"; } - public Merge(string repo, List 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 _outputHandler = null; } } diff --git a/src/Commands/MergeTool.cs b/src/Commands/MergeTool.cs index fc6d0d75..75b88bc9 100644 --- a/src/Commands/MergeTool.cs +++ b/src/Commands/MergeTool.cs @@ -6,66 +6,57 @@ namespace SourceGit.Commands { public static class MergeTool { - public static bool OpenForMerge(string repo, int toolType, string toolPath, string file) + public static bool OpenForMerge(string repo, string tool, string mergeCmd, string file) { + if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(mergeCmd)) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, "Invalid external merge tool settings!"); + }); + return false; + } + + if (!File.Exists(tool)) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!"); + }); + return false; + } + var cmd = new Command(); cmd.WorkingDirectory = repo; - cmd.Context = repo; - cmd.RaiseError = true; - - // NOTE: If no 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}"; - return cmd.Exec(); - } - - if (!File.Exists(toolPath)) - { - Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!")); - return false; - } - - var supported = Models.ExternalMerger.Supported.Find(x => x.Type == toolType); - if (supported == null) - { - Dispatcher.UIThread.Post(() => App.RaiseException(repo, "Invalid merge tool in preference setting!")); - 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.RaiseError = false; + cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{tool}\\\" {mergeCmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\""; return cmd.Exec(); } - public static bool OpenForDiff(string repo, int toolType, string toolPath, Models.DiffOption option) + public static bool OpenForDiff(string repo, string tool, string diffCmd, Models.DiffOption option) { + if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(diffCmd)) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, "Invalid external merge tool settings!"); + }); + return false; + } + + if (!File.Exists(tool)) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!"); + }); + return false; + } + var cmd = new Command(); cmd.WorkingDirectory = repo; - cmd.Context = repo; - cmd.RaiseError = true; - - if (toolType == 0) - { - cmd.Args = $"difftool -g --no-prompt {option}"; - return cmd.Exec(); - } - - if (!File.Exists(toolPath)) - { - Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!")); - return false; - } - - var supported = Models.ExternalMerger.Supported.Find(x => x.Type == toolType); - if (supported == null) - { - Dispatcher.UIThread.Post(() => App.RaiseException(repo, "Invalid merge tool in preference setting!")); - return false; - } - - cmd.Args = $"-c difftool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.DiffCmd}\" difftool --tool=sourcegit --no-prompt {option}"; + cmd.RaiseError = false; + cmd.Args = $"-c difftool.sourcegit.cmd=\"\\\"{tool}\\\" {diffCmd}\" difftool --tool=sourcegit --no-prompt {option}"; return cmd.Exec(); } } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index 698fbfce..d4f15dda 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -1,18 +1,37 @@ -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, Action outputHandler) { + _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - Args = "pull --verbose --progress "; + TraitErrorAsOutput = true; + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + Args = "-c credential.helper=manager "; + } + + Args += "pull --verbose --progress --tags "; if (useRebase) - Args += "--rebase=true "; - + Args += "--rebase "; Args += $"{remote} {branch}"; } + + protected override void OnReadline(string line) + { + _outputHandler?.Invoke(line); + } + + private readonly Action _outputHandler; } } diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index 8a5fe33c..b3e4814a 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -1,18 +1,30 @@ -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 onProgress) { WorkingDirectory = repo; Context = repo; - SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - Args = "push --progress --verbose "; + TraitErrorAsOutput = true; + _outputHandler = onProgress; + + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + Args = "-c credential.helper=manager "; + } + + Args += "push --progress --verbose "; if (withTags) Args += "--tags "; - if (checkSubmodules) - Args += "--recurse-submodules=check "; if (track) Args += "-u "; if (force) @@ -21,17 +33,57 @@ Args += $"{remote} {local}:{remoteBranch}"; } - public Push(string repo, string remote, string refname, bool isDelete) + /// + /// Only used to delete a remote branch!!!!!! + /// + /// + /// + /// + public Push(string repo, string remote, string branch) { WorkingDirectory = repo; Context = repo; - SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - Args = "push "; + TraitErrorAsOutput = true; + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + Args = "-c credential.helper=manager "; + } + + Args += $"push {remote} --delete {branch}"; + } + + public Push(string repo, string remote, string tag, bool isDelete) + { + WorkingDirectory = repo; + Context = repo; + + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + Args = "-c credential.helper=manager "; + } + + Args += "push "; if (isDelete) Args += "--delete "; - - Args += $"{remote} {refname}"; + Args += $"{remote} refs/tags/{tag}"; } + + protected override void OnReadline(string line) + { + _outputHandler?.Invoke(line); + } + + private readonly Action _outputHandler = null; } } diff --git a/src/Commands/QueryAssumeUnchangedFiles.cs b/src/Commands/QueryAssumeUnchangedFiles.cs deleted file mode 100644 index b5c23b0b..00000000 --- a/src/Commands/QueryAssumeUnchangedFiles.cs +++ /dev/null @@ -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 Result() - { - var outs = new List(); - 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; - } - } -} diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index d0ecd322..cc726fd9 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -1,83 +1,56 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; namespace SourceGit.Commands { - public class QueryBranches : Command + public partial class QueryBranches : Command { 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"; + + [GeneratedRegex(@"^(\d+)\s(\d+)$")] + private static partial Regex REG_AHEAD_BEHIND(); 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 Result(out int localBranchesCount) + public List Result() { - localBranchesCount = 0; + Exec(); - var branches = new List(); - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return branches; - - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - var remoteHeads = new Dictionary(); - foreach (var line in lines) + foreach (var b in _branches) { - var b = ParseLine(line); - if (b != null) + if (b.IsLocal && !string.IsNullOrEmpty(b.UpstreamTrackStatus)) { - 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)) + if (b.UpstreamTrackStatus == "=") { - b.IsUpstreamGone = false; - - if (b.TrackStatus == null) - b.TrackStatus = new QueryTrackStatus(WorkingDirectory, b.Head, upstreamHead).Result(); + b.UpstreamTrackStatus = string.Empty; } else { - b.IsUpstreamGone = true; - - if (b.TrackStatus == null) - b.TrackStatus = new Models.BranchTrackStatus(); + b.UpstreamTrackStatus = ParseTrackStatus(b.Name, b.Upstream); } } } - 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; - - branch.IsDetachedHead = refName.StartsWith(PREFIX_DETACHED_AT, StringComparison.Ordinal) || - refName.StartsWith(PREFIX_DETACHED_FROM, StringComparison.Ordinal); + return; if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { @@ -89,7 +62,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 +75,38 @@ 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; - - if (!branch.IsLocal || - string.IsNullOrEmpty(branch.Upstream) || - string.IsNullOrEmpty(parts[5]) || - parts[5].Equals("=", StringComparison.Ordinal)) - branch.TrackStatus = new Models.BranchTrackStatus(); - - return branch; + branch.Head = parts[1]; + branch.IsCurrent = parts[2] == "*"; + branch.Upstream = parts[3]; + branch.UpstreamTrackStatus = parts[4]; + _branches.Add(branch); } + + private string ParseTrackStatus(string local, string upstream) + { + var cmd = new Command(); + cmd.WorkingDirectory = WorkingDirectory; + cmd.Context = Context; + cmd.Args = $"rev-list --left-right --count {local}...{upstream}"; + + var rs = cmd.ReadToEnd(); + if (!rs.IsSuccess) + return string.Empty; + + var match = REG_AHEAD_BEHIND().Match(rs.StdOut); + if (!match.Success) + return string.Empty; + + var ahead = int.Parse(match.Groups[1].Value); + var behind = int.Parse(match.Groups[2].Value); + var track = ""; + if (ahead > 0) + track += $"{ahead}↑"; + if (behind > 0) + track += $" {behind}↓"; + return track.Trim(); + } + + private readonly List _branches = new List(); } } diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs deleted file mode 100644 index 4e99ce7a..00000000 --- a/src/Commands/QueryCommitChildren.cs +++ /dev/null @@ -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 Result() - { - var rs = ReadToEnd(); - var outs = new List(); - 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; - } -} diff --git a/src/Commands/QueryCommitFullMessage.cs b/src/Commands/QueryCommitFullMessage.cs deleted file mode 100644 index 36b6d1c7..00000000 --- a/src/Commands/QueryCommitFullMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace SourceGit.Commands -{ - public class QueryCommitFullMessage : Command - { - public QueryCommitFullMessage(string repo, string sha) - { - WorkingDirectory = repo; - Context = repo; - Args = $"show --no-show-signature --format=%B -s {sha}"; - } - - public string Result() - { - var rs = ReadToEnd(); - if (rs.IsSuccess) - return rs.StdOut.TrimEnd(); - return string.Empty; - } - } -} diff --git a/src/Commands/QueryCommitSignInfo.cs b/src/Commands/QueryCommitSignInfo.cs deleted file mode 100644 index 133949af..00000000 --- a/src/Commands/QueryCommitSignInfo.cs +++ /dev/null @@ -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] - }; - } - } -} diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 9e1d9918..7d6ad169 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -1,134 +1,190 @@ using System; using System.Collections.Generic; -using System.Text; namespace SourceGit.Commands { public class QueryCommits : Command { + private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; + private const string GPGSIG_END = " -----END PGP SIGNATURE-----"; + + private readonly List commits = new List(); + private Models.Commit current = null; + private bool isSkipingGpgsig = false; + private bool isHeadFounded = false; + private readonly bool findFirstMerged = true; + public QueryCommits(string repo, string limits, bool needFindHead = true) { 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}"; - _findFirstMerged = needFindHead; - } - - public QueryCommits(string repo, string filter, Models.CommitSearchMethod method, bool onlyCurrentBranch) - { - string search = onlyCurrentBranch ? string.Empty : "--branches --remotes "; - - if (method == Models.CommitSearchMethod.ByAuthor) - { - search += $"-i --author=\"{filter}\""; - } - else if (method == Models.CommitSearchMethod.ByCommitter) - { - search += $"-i --committer=\"{filter}\""; - } - else if (method == Models.CommitSearchMethod.ByMessage) - { - var argsBuilder = new StringBuilder(); - argsBuilder.Append(search); - - var words = filter.Split([' ', '\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}"; - _findFirstMerged = false; + Args = "log --date-order --decorate=full --pretty=raw " + limits; + findFirstMerged = needFindHead; } public List Result() { - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return _commits; + Exec(); - var nextPartIdx = 0; - var start = 0; - var end = rs.StdOut.IndexOf('\n', start); - while (end > 0) + if (current != null) { - var line = rs.StdOut.Substring(start, end - start); - switch (nextPartIdx) - { - case 0: - _current = new Models.Commit() { SHA = line }; - _commits.Add(_current); - break; - case 1: - ParseParent(line); - break; - case 2: - _current.ParseDecorators(line); - if (_current.IsMerged && !_isHeadFounded) - _isHeadFounded = true; - break; - case 3: - _current.Author = Models.User.FindOrAdd(line); - break; - case 4: - _current.AuthorTime = ulong.Parse(line); - break; - case 5: - _current.Committer = Models.User.FindOrAdd(line); - break; - case 6: - _current.CommitterTime = ulong.Parse(line); - break; - case 7: - _current.Subject = line; - nextPartIdx = -1; - break; - } - - nextPartIdx++; - - start = end + 1; - end = rs.StdOut.IndexOf('\n', start); + current.Message = current.Message.Trim(); + commits.Add(current); } - if (start < rs.StdOut.Length) - _current.Subject = rs.StdOut.Substring(start); - - if (_findFirstMerged && !_isHeadFounded && _commits.Count > 0) + if (findFirstMerged && !isHeadFounded && commits.Count > 0) + { MarkFirstMerged(); + } - return _commits; + return commits; } - private void ParseParent(string data) + protected override void OnReadline(string line) { - if (data.Length < 8) + if (isSkipingGpgsig) + { + if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) + isSkipingGpgsig = false; + return; + } + else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) + { + isSkipingGpgsig = true; + return; + } + + if (line.StartsWith("commit ", StringComparison.Ordinal)) + { + if (current != null) + { + current.Message = current.Message.Trim(); + commits.Add(current); + } + + current = new Models.Commit(); + line = line.Substring(7); + + var decoratorStart = line.IndexOf('(', StringComparison.Ordinal); + if (decoratorStart < 0) + { + current.SHA = line.Trim(); + } + else + { + current.SHA = line.Substring(0, decoratorStart).Trim(); + current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1)); + if (!isHeadFounded) + isHeadFounded = current.IsMerged; + } + + return; + } + + if (current == null) return; - _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); + if (line.StartsWith("tree ", StringComparison.Ordinal)) + { + return; + } + else if (line.StartsWith("parent ", StringComparison.Ordinal)) + { + current.Parents.Add(line.Substring("parent ".Length)); + } + else if (line.StartsWith("author ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); + current.Author = user; + current.AuthorTime = time; + } + else if (line.StartsWith("committer ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time); + current.Committer = user; + current.CommitterTime = time; + } + else if (string.IsNullOrEmpty(current.Subject)) + { + current.Subject = line.Trim(); + } + else + { + current.Message += (line.Trim() + "\n"); + } + } + + private bool ParseDecorators(List decorators, string data) + { + bool isHeadOfCurrent = false; + + var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var sub in subs) + { + var d = sub.Trim(); + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.Tag, + Name = d.Substring(15).Trim(), + }); + } + else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) + { + continue; + } + else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentBranchHead, + Name = d.Substring(19).Trim(), + }); + } + else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.LocalBranchHead, + Name = d.Substring(11).Trim(), + }); + } + else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.RemoteBranchHead, + Name = d.Substring(13).Trim(), + }); + } + } + + decorators.Sort((l, r) => + { + if (l.Type != r.Type) + { + return (int)l.Type - (int)r.Type; + } + else + { + return l.Name.CompareTo(r.Name); + } + }); + + return isHeadOfCurrent; } private void MarkFirstMerged() { - Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\""; + Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\""; var rs = ReadToEnd(); - var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if (shas.Length == 0) return; @@ -136,7 +192,7 @@ namespace SourceGit.Commands foreach (var sha in shas) set.Add(sha); - foreach (var c in _commits) + foreach (var c in commits) { if (set.Contains(c.SHA)) { @@ -145,10 +201,5 @@ namespace SourceGit.Commands } } } - - private List _commits = new List(); - private Models.Commit _current = null; - private bool _findFirstMerged = false; - private bool _isHeadFounded = false; } } diff --git a/src/Commands/QueryCommitsForInteractiveRebase.cs b/src/Commands/QueryCommitsForInteractiveRebase.cs deleted file mode 100644 index 9f238319..00000000 --- a/src/Commands/QueryCommitsForInteractiveRebase.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace SourceGit.Commands -{ - public class QueryCommitsForInteractiveRebase : Command - { - public QueryCommitsForInteractiveRebase(string repo, string on) - { - _boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----"; - - WorkingDirectory = repo; - Context = repo; - Args = $"log --date-order --no-show-signature --decorate=full --format=\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {on}..HEAD"; - } - - public List Result() - { - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return _commits; - - var nextPartIdx = 0; - var start = 0; - var end = rs.StdOut.IndexOf('\n', start); - while (end > 0) - { - var line = rs.StdOut.Substring(start, end - start); - switch (nextPartIdx) - { - case 0: - _current = new Models.InteractiveCommit(); - _current.Commit.SHA = line; - _commits.Add(_current); - break; - case 1: - ParseParent(line); - break; - case 2: - _current.Commit.ParseDecorators(line); - break; - case 3: - _current.Commit.Author = Models.User.FindOrAdd(line); - break; - case 4: - _current.Commit.AuthorTime = ulong.Parse(line); - break; - case 5: - _current.Commit.Committer = Models.User.FindOrAdd(line); - break; - case 6: - _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; - } - else - { - _current.Message = rs.StdOut.Substring(start); - end = rs.StdOut.Length - 2; - } - - nextPartIdx = -1; - break; - } - - nextPartIdx++; - - start = end + 1; - if (start >= rs.StdOut.Length - 1) - break; - - end = rs.StdOut.IndexOf('\n', start); - } - - return _commits; - } - - private void ParseParent(string data) - { - if (data.Length < 8) - return; - - _current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); - } - - private List _commits = []; - private Models.InteractiveCommit _current = null; - private readonly string _boundary; - } -} diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs index 83d0a575..f887859c 100644 --- a/src/Commands/QueryFileContent.cs +++ b/src/Commands/QueryFileContent.cs @@ -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; - } } } diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs index 30af7715..5ce7641e 100644 --- a/src/Commands/QueryFileSize.cs +++ b/src/Commands/QueryFileSize.cs @@ -4,6 +4,7 @@ namespace SourceGit.Commands { public partial class QueryFileSize : Command { + [GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")] private static partial Regex REG_FORMAT(); @@ -11,20 +12,27 @@ 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) { var match = REG_FORMAT().Match(rs.StdOut); if (match.Success) + { return long.Parse(match.Groups[1].Value); + } } return 0; } + + private readonly long _result = 0; } } diff --git a/src/Commands/QueryGitCommonDir.cs b/src/Commands/QueryGitCommonDir.cs deleted file mode 100644 index 1076243e..00000000 --- a/src/Commands/QueryGitCommonDir.cs +++ /dev/null @@ -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)); - } - } -} diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 788ed617..2f1e89b8 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -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,119 @@ 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 Result() { - var outs = new List(); - 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 " 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, Models.ChangeState.None); + break; + case "MM": + change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); + break; + case "MD": + change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); + break; + case "A": + change.Set(Models.ChangeState.Added, Models.ChangeState.None); + break; + case "AM": + change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); + break; + case "AD": + change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); + break; + case "D": + change.Set(Models.ChangeState.Deleted, Models.ChangeState.None); + break; + case "R": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.None); + break; + case "RM": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); + break; + case "RD": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); + break; + case "C": + change.Set(Models.ChangeState.Copied, Models.ChangeState.None); + break; + case "CM": + change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); + 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 _changes = new List(); } } diff --git a/src/Commands/QueryRefsContainsCommit.cs b/src/Commands/QueryRefsContainsCommit.cs deleted file mode 100644 index cabe1b50..00000000 --- a/src/Commands/QueryRefsContainsCommit.cs +++ /dev/null @@ -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 Result() - { - var rs = new List(); - - 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; - } - } -} diff --git a/src/Commands/QueryRemotes.cs b/src/Commands/QueryRemotes.cs index 7afec74d..b5b41b4a 100644 --- a/src/Commands/QueryRemotes.cs +++ b/src/Commands/QueryRemotes.cs @@ -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 Result() { - var outs = new List(); - 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 _loaded = new List(); } } diff --git a/src/Commands/QueryRepositoryRootPath.cs b/src/Commands/QueryRepositoryRootPath.cs index 016621c8..1eef5af8 100644 --- a/src/Commands/QueryRepositoryRootPath.cs +++ b/src/Commands/QueryRepositoryRootPath.cs @@ -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(); } } } diff --git a/src/Commands/QueryRevisionByRefName.cs b/src/Commands/QueryRevisionByRefName.cs deleted file mode 100644 index 7fb4ecfa..00000000 --- a/src/Commands/QueryRevisionByRefName.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Commands/QueryRevisionFileNames.cs b/src/Commands/QueryRevisionFileNames.cs deleted file mode 100644 index c6fd7373..00000000 --- a/src/Commands/QueryRevisionFileNames.cs +++ /dev/null @@ -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 Result() - { - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return []; - - var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); - var outs = new List(); - foreach (var line in lines) - outs.Add(line); - return outs; - } - } -} diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs index de3406e8..7a3db057 100644 --- a/src/Commands/QueryRevisionObjects.cs +++ b/src/Commands/QueryRevisionObjects.cs @@ -5,42 +5,25 @@ namespace SourceGit.Commands { public partial class QueryRevisionObjects : Command { + [GeneratedRegex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$")] private static partial Regex REG_FORMAT(); + private readonly List objects = new List(); - public QueryRevisionObjects(string repo, string sha, string parentFolder) + public QueryRevisionObjects(string repo, string sha) { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree -z {sha}"; - - if (!string.IsNullOrEmpty(parentFolder)) - Args += $" -- \"{parentFolder}\""; + Args = $"ls-tree -r {sha}"; } public List 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)); - } - - return _objects; + Exec(); + return objects; } - private void Parse(string line) + protected override void OnReadline(string line) { var match = REG_FORMAT().Match(line); if (!match.Success) @@ -67,9 +50,7 @@ namespace SourceGit.Commands break; } - _objects.Add(obj); + objects.Add(obj); } - - private List _objects = new List(); } } diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs deleted file mode 100644 index 35289ec5..00000000 --- a/src/Commands/QuerySingleCommit.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace SourceGit.Commands -{ - public class QuerySingleCommit : Command - { - public QuerySingleCommit(string repo, string sha) - { - 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}"; - } - - public Models.Commit Result() - { - var rs = ReadToEnd(); - if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut)) - { - var commit = new Models.Commit(); - var lines = rs.StdOut.Split('\n'); - if (lines.Length < 8) - return null; - - commit.SHA = lines[0]; - if (!string.IsNullOrEmpty(lines[1])) - commit.Parents.AddRange(lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries)); - if (!string.IsNullOrEmpty(lines[2])) - commit.ParseDecorators(lines[2]); - commit.Author = Models.User.FindOrAdd(lines[3]); - commit.AuthorTime = ulong.Parse(lines[4]); - commit.Committer = Models.User.FindOrAdd(lines[5]); - commit.CommitterTime = ulong.Parse(lines[6]); - commit.Subject = lines[7]; - - return commit; - } - - return null; - } - } -} diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs deleted file mode 100644 index 78980401..00000000 --- a/src/Commands/QueryStagedChangesWithAmend.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; - -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(.*)$")] - 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) - { - WorkingDirectory = repo; - Context = repo; - Args = $"diff-index --cached -M {parent}"; - _parent = parent; - } - - public List Result() - { - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return []; - - var changes = new List(); - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - var match = REG_FORMAT2().Match(line); - if (match.Success) - { - var change = new Models.Change() - { - Path = match.Groups[3].Value, - DataForAmend = new Models.ChangeDataForAmend() - { - 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; - } - changes.Add(change); - } - } - - return changes; - } - - private readonly string _parent; - } -} diff --git a/src/Commands/QueryStashChanges.cs b/src/Commands/QueryStashChanges.cs new file mode 100644 index 00000000..bf61ca2d --- /dev/null +++ b/src/Commands/QueryStashChanges.cs @@ -0,0 +1,61 @@ +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 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 _changes = new List(); + } +} diff --git a/src/Commands/QueryStashes.cs b/src/Commands/QueryStashes.cs index b4067aaf..5362f87b 100644 --- a/src/Commands/QueryStashes.cs +++ b/src/Commands/QueryStashes.cs @@ -1,73 +1,64 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; namespace SourceGit.Commands { - public class QueryStashes : Command + public partial class QueryStashes : Command { + + [GeneratedRegex(@"^Reflog: refs/(stash@\{\d+\}).*$")] + private static partial Regex REG_STASH(); + public QueryStashes(string repo) { WorkingDirectory = repo; Context = repo; - Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s"; + Args = "stash list --pretty=raw"; } public List Result() { - var outs = new List(); - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return outs; + Exec(); + if (_current != null) + _stashes.Add(_current); + return _stashes; + } - var nextPartIdx = 0; - var start = 0; - var end = rs.StdOut.IndexOf('\n', start); - while (end > 0) + protected override void OnReadline(string line) + { + if (line.StartsWith("commit ", StringComparison.Ordinal)) { - 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); + if (_current != null && !string.IsNullOrEmpty(_current.Name)) + _stashes.Add(_current); + _current = new Models.Stash() { SHA = line.Substring(7, 8) }; + return; } - if (start < rs.StdOut.Length) - _current.Message = rs.StdOut.Substring(start); - - return outs; - } - - private void ParseParent(string data) - { - if (data.Length < 8) + if (_current == null) return; - _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); + if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) + { + var match = REG_STASH().Match(line); + if (match.Success) + _current.Name = match.Groups[1].Value; + } + else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal)) + { + _current.Message = line.Substring(16); + } + else if (line.StartsWith("author ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); + _current.Author = user; + _current.Time = time; + } } + private readonly List _stashes = new List(); private Models.Stash _current = null; } } diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 663c0ea0..622de2fc 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -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 Result() + public List Result() { - var submodules = new List(); - var rs = ReadToEnd(); - - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - var map = new Dictionary(); - 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(); - 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 _submodules = new List(); } } diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 4b706439..0b5b747f 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -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 = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags"; } public List Result() { - var tags = new List(); - 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(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries); + if (subs.Length == 2) + { + _loaded.Add(new Models.Tag() + { + Name = subs[0], + SHA = subs[1], + }); + } + else if (subs.Length == 3) + { + _loaded.Add(new Models.Tag() + { + Name = subs[0], + SHA = subs[2], + }); + } + } + + private readonly List _loaded = new List(); } } diff --git a/src/Commands/QueryTrackStatus.cs b/src/Commands/QueryTrackStatus.cs deleted file mode 100644 index e7e1f1c9..00000000 --- a/src/Commands/QueryTrackStatus.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace SourceGit.Commands -{ - public class QueryTrackStatus : Command - { - public QueryTrackStatus(string repo, string local, string upstream) - { - WorkingDirectory = repo; - Context = repo; - Args = $"rev-list --left-right {local}...{upstream}"; - } - - public Models.BranchTrackStatus Result() - { - var status = new Models.BranchTrackStatus(); - - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return status; - - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - if (line[0] == '>') - status.Behind.Add(line.Substring(1)); - else - status.Ahead.Add(line.Substring(1)); - } - - return status; - } - } -} diff --git a/src/Commands/QueryUpdatableSubmodules.cs b/src/Commands/QueryUpdatableSubmodules.cs deleted file mode 100644 index 03f4a24d..00000000 --- a/src/Commands/QueryUpdatableSubmodules.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Commands -{ - public partial class QueryUpdatableSubmodules : Command - { - [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] - private static partial Regex REG_FORMAT_STATUS(); - - public QueryUpdatableSubmodules(string repo) - { - WorkingDirectory = repo; - Context = repo; - Args = "submodule status"; - } - - public List Result() - { - var submodules = new List(); - var rs = ReadToEnd(); - - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - var match = REG_FORMAT_STATUS().Match(line); - if (match.Success) - { - var stat = match.Groups[1].Value; - var path = match.Groups[3].Value; - if (!stat.StartsWith(' ')) - submodules.Add(path); - } - } - - return submodules; - } - } -} diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs index 2ec50f3c..d08d55ad 100644 --- a/src/Commands/Rebase.cs +++ b/src/Commands/Rebase.cs @@ -12,15 +12,4 @@ Args += basedOn; } } - - public class InteractiveRebase : Command - { - public InteractiveRebase(string repo, string basedOn) - { - WorkingDirectory = repo; - Context = repo; - Editor = EditorType.RebaseEditor; - Args = $"rebase -i --autosquash {basedOn}"; - } - } } diff --git a/src/Commands/Remote.cs b/src/Commands/Remote.cs index beaf412b..46aa37e3 100644 --- a/src/Commands/Remote.cs +++ b/src/Commands/Remote.cs @@ -32,27 +32,10 @@ return Exec(); } - public string GetURL(string name, bool isPush) + public bool SetURL(string name, string url) { - Args = "remote get-url" + (isPush ? " --push " : " ") + name; - - var rs = ReadToEnd(); - return rs.IsSuccess ? rs.StdOut.Trim() : string.Empty; - } - - public bool SetURL(string name, string url, bool isPush) - { - Args = "remote set-url" + (isPush ? " --push " : " ") + $"{name} {url}"; + Args = $"remote set-url {name} {url}"; return Exec(); } - - public bool HasBranch(string remote, string branch) - { - SSHKey = new Config(WorkingDirectory).Get($"remote.{remote}.sshkey"); - Args = $"ls-remote {remote} {branch}"; - - var rs = ReadToEnd(); - return rs.IsSuccess && rs.StdOut.Trim().Length > 0; - } } } diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs index 6a54533b..da272135 100644 --- a/src/Commands/Reset.cs +++ b/src/Commands/Reset.cs @@ -1,7 +1,33 @@ -namespace SourceGit.Commands +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { public class Reset : Command { + public Reset(string repo) + { + WorkingDirectory = repo; + Context = repo; + Args = "reset"; + } + + public Reset(string repo, List changes) + { + WorkingDirectory = repo; + Context = repo; + + var builder = new StringBuilder(); + builder.Append("reset --"); + foreach (var c in changes) + { + builder.Append(" \""); + builder.Append(c.Path); + builder.Append("\""); + } + Args = builder.ToString(); + } + public Reset(string repo, string revision, string mode) { WorkingDirectory = repo; diff --git a/src/Commands/Restore.cs b/src/Commands/Restore.cs index 663ea975..12a2eaa1 100644 --- a/src/Commands/Restore.cs +++ b/src/Commands/Restore.cs @@ -1,52 +1,22 @@ -using System.Text; +using System.Collections.Generic; +using System.Text; namespace SourceGit.Commands { public class Restore : Command { - /// - /// Only used for single staged change. - /// - /// - /// - public Restore(string repo, Models.Change stagedChange) + public Restore(string repo, List files, string extra) { WorkingDirectory = repo; Context = repo; - var builder = new StringBuilder(); - builder.Append("restore --staged -- \""); - builder.Append(stagedChange.Path); - builder.Append('"'); - - if (stagedChange.Index == Models.ChangeState.Renamed) - { - builder.Append(" \""); - builder.Append(stagedChange.OriginalPath); - builder.Append('"'); - } - - Args = builder.ToString(); - } - - /// - /// Restore changes given in a path-spec file. - /// - /// - /// - /// - public Restore(string repo, string pathspecFile, bool isStaged) - { - WorkingDirectory = repo; - Context = repo; - - var builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(); builder.Append("restore "); - builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules "); - builder.Append("--pathspec-from-file=\""); - builder.Append(pathspecFile); - builder.Append('"'); - + if (!string.IsNullOrEmpty(extra)) + builder.Append(extra).Append(" "); + builder.Append("--"); + foreach (var f in files) + builder.Append(' ').Append('"').Append(f).Append('"'); Args = builder.ToString(); } } diff --git a/src/Commands/Revert.cs b/src/Commands/Revert.cs index 2e7afd11..b6c3913a 100644 --- a/src/Commands/Revert.cs +++ b/src/Commands/Revert.cs @@ -6,9 +6,9 @@ { WorkingDirectory = repo; Context = repo; - Args = $"revert -m 1 {commit} --no-edit"; + Args = $"revert {commit} --no-edit"; if (!autoCommit) - Args += " --no-commit"; + Args += " --no-commit"; } } } diff --git a/src/Commands/SaveChangesAsPatch.cs b/src/Commands/SaveChangesAsPatch.cs index b10037a1..409127ba 100644 --- a/src/Commands/SaveChangesAsPatch.cs +++ b/src/Commands/SaveChangesAsPatch.cs @@ -9,7 +9,7 @@ namespace SourceGit.Commands { public static class SaveChangesAsPatch { - public static bool ProcessLocalChanges(string repo, List changes, bool isUnstaged, string saveTo) + public static bool Exec(string repo, List changes, bool isUnstaged, string saveTo) { using (var sw = File.Create(saveTo)) { @@ -23,33 +23,6 @@ namespace SourceGit.Commands return true; } - public static bool ProcessRevisionCompareChanges(string repo, List changes, string baseRevision, string targetRevision, string saveTo) - { - using (var sw = File.Create(saveTo)) - { - foreach (var change in changes) - { - if (!ProcessSingleChange(repo, new Models.DiffOption(baseRevision, targetRevision, change), sw)) - return false; - } - } - - return true; - } - - public static bool ProcessStashChanges(string repo, List opts, string saveTo) - { - using (var sw = File.Create(saveTo)) - { - foreach (var opt in opts) - { - if (!ProcessSingleChange(repo, opt, sw)) - return false; - } - } - return true; - } - private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer) { var starter = new ProcessStartInfo(); diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 550844ef..6c200940 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -10,11 +10,15 @@ namespace SourceGit.Commands { public static void Run(string repo, string revision, string file, string saveTo) { - var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result(); + var isLFSFiltered = new IsLFSFiltered(repo, file).Result(); if (isLFSFiltered) { - var pointerStream = QueryFileContent.Run(repo, revision, file); - ExecCmd(repo, $"lfs smudge", saveTo, pointerStream); + var tmpFile = saveTo + ".tmp"; + if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) + { + ExecCmd(repo, $"lfs smudge", saveTo, tmpFile); + } + File.Delete(tmpFile); } else { @@ -22,7 +26,7 @@ namespace SourceGit.Commands } } - private static bool ExecCmd(string repo, string args, string outputFile, Stream input = null) + private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) { var starter = new ProcessStartInfo(); starter.WorkingDirectory = repo; @@ -41,8 +45,21 @@ namespace SourceGit.Commands { var proc = new Process() { StartInfo = starter }; proc.Start(); - if (input != null) - proc.StandardInput.Write(new StreamReader(input).ReadToEnd()); + + if (inputFile != null) + { + using (StreamReader sr = new StreamReader(inputFile)) + { + while (true) + { + var line = sr.ReadLine(); + if (line == null) + break; + proc.StandardInput.WriteLine(line); + } + } + } + proc.StandardOutput.BaseStream.CopyTo(sw); proc.WaitForExit(); var rs = proc.ExitCode == 0; diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs index 7d1a269b..3e784f60 100644 --- a/src/Commands/Stash.cs +++ b/src/Commands/Stash.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Text; +using System.IO; namespace SourceGit.Commands { @@ -11,84 +11,65 @@ namespace SourceGit.Commands Context = repo; } - public bool Push(string message, bool includeUntracked = true, bool keepIndex = false) + public bool Push(string message) { - var builder = new StringBuilder(); - builder.Append("stash push "); - if (includeUntracked) - builder.Append("--include-untracked "); - if (keepIndex) - builder.Append("--keep-index "); - builder.Append("-m \""); - builder.Append(message); - builder.Append("\""); - - Args = builder.ToString(); + Args = $"stash push -m \"{message}\""; return Exec(); } - public bool Push(string message, List changes, bool keepIndex) + public bool Push(List changes, string message) { - var builder = new StringBuilder(); - builder.Append("stash push --include-untracked "); - if (keepIndex) - builder.Append("--keep-index "); - builder.Append("-m \""); - builder.Append(message); - builder.Append("\" -- "); + var temp = Path.GetTempFileName(); + var stream = new FileStream(temp, FileMode.Create); + var writer = new StreamWriter(stream); + var needAdd = new List(); foreach (var c in changes) - builder.Append($"\"{c.Path}\" "); + { + writer.WriteLine(c.Path); - Args = builder.ToString(); - return Exec(); + if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) + { + needAdd.Add(c); + if (needAdd.Count > 10) + { + new Add(WorkingDirectory, needAdd).Exec(); + needAdd.Clear(); + } + } + } + if (needAdd.Count > 0) + { + new Add(WorkingDirectory, needAdd).Exec(); + needAdd.Clear(); + } + + writer.Flush(); + stream.Flush(); + writer.Close(); + stream.Close(); + + Args = $"stash push -m \"{message}\" --pathspec-from-file=\"{temp}\""; + var succ = Exec(); + File.Delete(temp); + return succ; } - public bool Push(string message, string pathspecFromFile, bool keepIndex) + public bool Apply(string name) { - var builder = new StringBuilder(); - builder.Append("stash push --include-untracked --pathspec-from-file=\""); - builder.Append(pathspecFromFile); - builder.Append("\" "); - if (keepIndex) - builder.Append("--keep-index "); - builder.Append("-m \""); - builder.Append(message); - builder.Append("\""); - - Args = builder.ToString(); - return Exec(); - } - - public bool PushOnlyStaged(string message, bool keepIndex) - { - var builder = new StringBuilder(); - builder.Append("stash push --staged "); - if (keepIndex) - builder.Append("--keep-index "); - builder.Append("-m \""); - builder.Append(message); - builder.Append("\""); - Args = builder.ToString(); - return Exec(); - } - - public bool Apply(string name, bool restoreIndex) - { - var opts = restoreIndex ? "--index" : string.Empty; - Args = $"stash apply -q {opts} \"{name}\""; + Args = $"stash apply -q {name}"; return Exec(); } public bool Pop(string name) { - Args = $"stash pop -q --index \"{name}\""; + Args = $"stash pop -q {name}"; return Exec(); } public bool Drop(string name) { - Args = $"stash drop -q \"{name}\""; + Args = $"stash drop -q {name}"; return Exec(); } diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index e11c1740..85f5a4fb 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -1,48 +1,39 @@ -using System; +using System; namespace SourceGit.Commands { public class Statistics : Command { - public Statistics(string repo, int max) + public Statistics(string repo) { + _statistics = new Models.Statistics(); + WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE"; + Args = $"log --date-order --branches --remotes --since=\"{_statistics.Since()}\" --pretty=format:\"%ct$%cn\""; } public Models.Statistics Result() { - var statistics = new Models.Statistics(); - var rs = ReadToEnd(); - if (!rs.IsSuccess) - return statistics; - - var start = 0; - var end = rs.StdOut.IndexOf('\n', start); - while (end > 0) - { - ParseLine(statistics, rs.StdOut.Substring(start, end - start)); - start = end + 1; - end = rs.StdOut.IndexOf('\n', start); - } - - if (start < rs.StdOut.Length) - ParseLine(statistics, rs.StdOut.Substring(start)); - - statistics.Complete(); - return statistics; + Exec(); + _statistics.Complete(); + return _statistics; } - private void ParseLine(Models.Statistics statistics, string line) + protected override void OnReadline(string line) { var dateEndIdx = line.IndexOf('$', StringComparison.Ordinal); if (dateEndIdx == -1) return; - var dateStr = line.AsSpan(0, dateEndIdx); - if (double.TryParse(dateStr, out var date)) - statistics.AddCommit(line.Substring(dateEndIdx + 1), date); + var dateStr = line.Substring(0, dateEndIdx); + var date = 0.0; + if (!double.TryParse(dateStr, out date)) + return; + + _statistics.AddCommit(line.Substring(dateEndIdx + 1), date); } + + private readonly Models.Statistics _statistics = null; } } diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs index 025d035a..428c10d1 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text; +using System; namespace SourceGit.Commands { @@ -11,56 +10,46 @@ namespace SourceGit.Commands Context = repo; } - public bool Add(string url, string relativePath, bool recursive) + public bool Add(string url, string relativePath, bool recursive, Action outputHandler) { - Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\""; + _outputHandler = outputHandler; + Args = $"submodule add {url} {relativePath}"; if (!Exec()) return false; if (recursive) { - Args = $"submodule update --init --recursive -- \"{relativePath}\""; + Args = $"submodule update --init --recursive -- {relativePath}"; return Exec(); } else { - Args = $"submodule update --init -- \"{relativePath}\""; + Args = $"submodule update --init -- {relativePath}"; return true; } } - public bool Update(List modules, bool init, bool recursive, bool useRemote = false) + public bool Update() { - var builder = new StringBuilder(); - builder.Append("submodule update"); - - if (init) - builder.Append(" --init"); - if (recursive) - builder.Append(" --recursive"); - if (useRemote) - builder.Append(" --remote"); - if (modules.Count > 0) - { - builder.Append(" --"); - foreach (var module in modules) - builder.Append($" \"{module}\""); - } - - Args = builder.ToString(); + Args = $"submodule update --rebase --remote"; return Exec(); } - public bool Deinit(string module, bool force) + public bool Delete(string relativePath) { - Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\""; + Args = $"submodule deinit -f {relativePath}"; + if (!Exec()) + return false; + + Args = $"rm -rf {relativePath}"; return Exec(); } - public bool Delete(string module) + protected override void OnReadline(string line) { - Args = $"rm -rf \"{module}\""; - return Exec(); + _outputHandler?.Invoke(line); } + + private Action _outputHandler; } } diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index 017afea0..f96d3bc7 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -1,51 +1,49 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; namespace SourceGit.Commands { public static class Tag { - public static bool Add(string repo, string name, string basedOn, Models.ICommandLog log) + public static bool Add(string repo, string name, string basedOn, string message) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"tag --no-sign {name} {basedOn}"; - cmd.Log = log; - return cmd.Exec(); - } - - public static bool Add(string repo, string name, string basedOn, string message, bool sign, Models.ICommandLog log) - { - var param = sign ? "--sign -a" : "--no-sign -a"; - var cmd = new Command(); - cmd.WorkingDirectory = repo; - cmd.Context = repo; - cmd.Args = $"tag {param} {name} {basedOn} "; - cmd.Log = log; + cmd.Args = $"tag -a {name} {basedOn} "; if (!string.IsNullOrEmpty(message)) { string tmp = Path.GetTempFileName(); File.WriteAllText(tmp, message); cmd.Args += $"-F \"{tmp}\""; - - var succ = cmd.Exec(); - File.Delete(tmp); - return succ; + } + else + { + cmd.Args += $"-m {name}"; } - cmd.Args += $"-m {name}"; return cmd.Exec(); } - public static bool Delete(string repo, string name, Models.ICommandLog log) + public static bool Delete(string repo, string name, List remotes) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"tag --delete {name}"; - cmd.Log = log; - return cmd.Exec(); + if (!cmd.Exec()) + return false; + + if (remotes != null) + { + foreach (var r in remotes) + { + new Push(repo, r.Name, name, true).Exec(); + } + } + + return true; } } } diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UnstageChangesForAmend.cs deleted file mode 100644 index 19def067..00000000 --- a/src/Commands/UnstageChangesForAmend.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; - -using Avalonia.Threading; - -namespace SourceGit.Commands -{ - public class UnstageChangesForAmend - { - public UnstageChangesForAmend(string repo, List changes) - { - _repo = repo; - - foreach (var c in changes) - { - if (c.Index == Models.ChangeState.Renamed) - { - _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); - _patchBuilder.Append(c.Path); - _patchBuilder.Append("\0100644 "); - _patchBuilder.Append(c.DataForAmend.ObjectHash); - _patchBuilder.Append("\t"); - _patchBuilder.Append(c.OriginalPath); - } - else if (c.Index == Models.ChangeState.Added) - { - _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); - _patchBuilder.Append(c.Path); - } - else if (c.Index == Models.ChangeState.Deleted) - { - _patchBuilder.Append("100644 "); - _patchBuilder.Append(c.DataForAmend.ObjectHash); - _patchBuilder.Append("\t"); - _patchBuilder.Append(c.Path); - } - else - { - _patchBuilder.Append(c.DataForAmend.FileMode); - _patchBuilder.Append(" "); - _patchBuilder.Append(c.DataForAmend.ObjectHash); - _patchBuilder.Append("\t"); - _patchBuilder.Append(c.Path); - } - - _patchBuilder.Append("\n"); - } - } - - public bool Exec() - { - var starter = new ProcessStartInfo(); - starter.WorkingDirectory = _repo; - starter.FileName = Native.OS.GitExecutable; - starter.Arguments = "-c core.editor=true update-index --index-info"; - starter.UseShellExecute = false; - starter.CreateNoWindow = true; - starter.WindowStyle = ProcessWindowStyle.Hidden; - starter.RedirectStandardInput = true; - starter.RedirectStandardOutput = false; - starter.RedirectStandardError = true; - - try - { - var proc = new Process() { StartInfo = starter }; - proc.Start(); - proc.StandardInput.Write(_patchBuilder.ToString()); - proc.StandardInput.Close(); - - var err = proc.StandardError.ReadToEnd(); - proc.WaitForExit(); - var rs = proc.ExitCode == 0; - proc.Close(); - - if (!rs) - Dispatcher.UIThread.Invoke(() => App.RaiseException(_repo, err)); - - return rs; - } - catch (Exception e) - { - Dispatcher.UIThread.Invoke(() => - { - App.RaiseException(_repo, "Failed to unstage changes: " + e.Message); - }); - return false; - } - } - - private string _repo = ""; - private StringBuilder _patchBuilder = new StringBuilder(); - } -} diff --git a/src/Commands/Version.cs b/src/Commands/Version.cs new file mode 100644 index 00000000..ed7c6892 --- /dev/null +++ b/src/Commands/Version.cs @@ -0,0 +1,19 @@ +namespace SourceGit.Commands +{ + public class Version : Command + { + public Version() + { + Args = "--version"; + RaiseError = false; + } + + public string Query() + { + var rs = ReadToEnd(); + if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) + return string.Empty; + return rs.StdOut.Trim().Substring("git version ".Length); + } + } +} diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs deleted file mode 100644 index 1198a443..00000000 --- a/src/Commands/Worktree.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace SourceGit.Commands -{ - public class Worktree : Command - { - public Worktree(string repo) - { - WorkingDirectory = repo; - Context = repo; - } - - public List List() - { - Args = "worktree list --porcelain"; - - var rs = ReadToEnd(); - var worktrees = new List(); - var last = null as Models.Worktree; - if (rs.IsSuccess) - { - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - if (line.StartsWith("worktree ", StringComparison.Ordinal)) - { - last = new Models.Worktree() { FullPath = line.Substring(9).Trim() }; - last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath); - worktrees.Add(last); - } - else if (line.StartsWith("bare", StringComparison.Ordinal)) - { - last!.IsBare = true; - } - else if (line.StartsWith("HEAD ", StringComparison.Ordinal)) - { - last!.Head = line.Substring(5).Trim(); - } - else if (line.StartsWith("branch ", StringComparison.Ordinal)) - { - last!.Branch = line.Substring(7).Trim(); - } - else if (line.StartsWith("detached", StringComparison.Ordinal)) - { - last!.IsDetached = true; - } - else if (line.StartsWith("locked", StringComparison.Ordinal)) - { - last!.IsLocked = true; - } - } - } - - return worktrees; - } - - public bool Add(string fullpath, string name, bool createNew, string tracking) - { - Args = "worktree add "; - - if (!string.IsNullOrEmpty(tracking)) - Args += "--track "; - - if (!string.IsNullOrEmpty(name)) - { - if (createNew) - Args += $"-b {name} "; - else - Args += $"-B {name} "; - } - - Args += $"\"{fullpath}\" "; - - if (!string.IsNullOrEmpty(tracking)) - Args += tracking; - else if (!string.IsNullOrEmpty(name) && !createNew) - Args += name; - - return Exec(); - } - - public bool Prune() - { - Args = "worktree prune -v"; - return Exec(); - } - - public bool Lock(string fullpath) - { - Args = $"worktree lock \"{fullpath}\""; - return Exec(); - } - - public bool Unlock(string fullpath) - { - Args = $"worktree unlock \"{fullpath}\""; - return Exec(); - } - - public bool Remove(string fullpath, bool force) - { - if (force) - Args = $"worktree remove -f \"{fullpath}\""; - else - Args = $"worktree remove \"{fullpath}\""; - - return Exec(); - } - } -} diff --git a/src/Converters/BookmarkConverters.cs b/src/Converters/BookmarkConverters.cs new file mode 100644 index 00000000..9896d9aa --- /dev/null +++ b/src/Converters/BookmarkConverters.cs @@ -0,0 +1,14 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace SourceGit.Converters +{ + public static class BookmarkConverters + { + public static readonly FuncValueConverter ToBrush = + new FuncValueConverter(bookmark => Models.Bookmarks.Brushes[bookmark]); + + public static readonly FuncValueConverter ToStrokeThickness = + new FuncValueConverter(bookmark => bookmark == 0 ? 1.0 : 0); + } +} diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs index 3563fb37..bfe3bf41 100644 --- a/src/Converters/BoolConverters.cs +++ b/src/Converters/BoolConverters.cs @@ -5,10 +5,10 @@ namespace SourceGit.Converters { public static class BoolConverters { - public static readonly FuncValueConverter ToPageTabWidth = - new FuncValueConverter(x => x ? 200 : double.NaN); + public static readonly FuncValueConverter ToCommitOpacity = + new FuncValueConverter(x => x ? 1 : 0.5); - public static readonly FuncValueConverter IsBoldToFontWeight = - new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Normal); + public static readonly FuncValueConverter ToCommitFontWeight = + new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Regular); } } diff --git a/src/Converters/BranchConverters.cs b/src/Converters/BranchConverters.cs new file mode 100644 index 00000000..d20ed89f --- /dev/null +++ b/src/Converters/BranchConverters.cs @@ -0,0 +1,10 @@ +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class BranchConverters + { + public static readonly FuncValueConverter ToName = + new FuncValueConverter(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}"); + } +} diff --git a/src/Converters/ChangeViewModeConverters.cs b/src/Converters/ChangeViewModeConverters.cs new file mode 100644 index 00000000..01bc1774 --- /dev/null +++ b/src/Converters/ChangeViewModeConverters.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace SourceGit.Converters +{ + public static class ChangeViewModeConverters + { + public static readonly FuncValueConverter ToIcon = + new FuncValueConverter(v => + { + switch (v) + { + case Models.ChangeViewMode.List: + return App.Current?.FindResource("Icons.List") as StreamGeometry; + case Models.ChangeViewMode.Grid: + return App.Current?.FindResource("Icons.Grid") as StreamGeometry; + default: + return App.Current?.FindResource("Icons.Tree") as StreamGeometry; + } + }); + + public static readonly FuncValueConverter IsList = + new FuncValueConverter(v => v == Models.ChangeViewMode.List); + + public static readonly FuncValueConverter IsGrid = + new FuncValueConverter(v => v == Models.ChangeViewMode.Grid); + + public static readonly FuncValueConverter IsTree = + new FuncValueConverter(v => v == Models.ChangeViewMode.Tree); + } +} diff --git a/src/Converters/DecoratorTypeConverters.cs b/src/Converters/DecoratorTypeConverters.cs new file mode 100644 index 00000000..eb016360 --- /dev/null +++ b/src/Converters/DecoratorTypeConverters.cs @@ -0,0 +1,43 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace SourceGit.Converters +{ + public static class DecoratorTypeConverters + { + public static readonly FuncValueConverter ToBackground = + new FuncValueConverter(v => + { + if (v == Models.DecoratorType.Tag) + return Models.DecoratorResources.Backgrounds[0]; + return Models.DecoratorResources.Backgrounds[1]; + }); + + public static readonly FuncValueConverter ToIcon = + new FuncValueConverter(v => + { + var key = "Icons.Tag"; + switch (v) + { + case Models.DecoratorType.CurrentBranchHead: + key = "Icons.Check"; + break; + case Models.DecoratorType.RemoteBranchHead: + key = "Icons.Remote"; + break; + case Models.DecoratorType.LocalBranchHead: + key = "Icons.Branch"; + break; + default: + break; + } + + return Application.Current?.FindResource(key) as StreamGeometry; + }); + + public static readonly FuncValueConverter ToFontWeight = + new FuncValueConverter(v => v == Models.DecoratorType.CurrentBranchHead ? FontWeight.Bold : FontWeight.Regular); + } +} diff --git a/src/Converters/DoubleConverters.cs b/src/Converters/DoubleConverters.cs deleted file mode 100644 index 5b7c0a03..00000000 --- a/src/Converters/DoubleConverters.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class DoubleConverters - { - public static readonly FuncValueConverter Increase = - new FuncValueConverter(v => v + 1.0); - - public static readonly FuncValueConverter Decrease = - new FuncValueConverter(v => v - 1.0); - - public static readonly FuncValueConverter ToPercentage = - new FuncValueConverter(v => (v * 100).ToString("F3") + "%"); - - public static readonly FuncValueConverter OneMinusToPercentage = - new FuncValueConverter(v => ((1.0 - v) * 100).ToString("F3") + "%"); - } -} diff --git a/src/Converters/FilterModeConverters.cs b/src/Converters/FilterModeConverters.cs deleted file mode 100644 index c486af5e..00000000 --- a/src/Converters/FilterModeConverters.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace SourceGit.Converters -{ - public static class FilterModeConverters - { - public static readonly FuncValueConverter ToBorderBrush = - new FuncValueConverter(v => - { - switch (v) - { - case Models.FilterMode.Included: - return Brushes.Green; - case Models.FilterMode.Excluded: - return Brushes.Red; - default: - return Brushes.Transparent; - } - }); - } -} diff --git a/src/Converters/FontSizeModifyConverters.cs b/src/Converters/FontSizeModifyConverters.cs new file mode 100644 index 00000000..4c885e38 --- /dev/null +++ b/src/Converters/FontSizeModifyConverters.cs @@ -0,0 +1,13 @@ +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class FontSizeModifyConverters + { + public static readonly FuncValueConverter Increase = + new FuncValueConverter(v => v + 1.0); + + public static readonly FuncValueConverter Decrease = + new FuncValueConverter(v => v - 1.0); + } +} diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index f21c5d24..820f62c5 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -1,7 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Data.Converters; -using Avalonia.Media; +using Avalonia.Data.Converters; namespace SourceGit.Converters { @@ -10,9 +7,6 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsGreaterThanZero = new FuncValueConverter(v => v > 0); - public static readonly FuncValueConverter IsGreaterThanFour = - new FuncValueConverter(v => v > 4); - public static readonly FuncValueConverter IsZero = new FuncValueConverter(v => v == 0); @@ -21,23 +15,5 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsNotOne = new FuncValueConverter(v => v != 1); - - public static readonly FuncValueConverter IsSubjectLengthBad = - new FuncValueConverter(v => v > ViewModels.Preferences.Instance.SubjectGuideLength); - - public static readonly FuncValueConverter IsSubjectLengthGood = - new FuncValueConverter(v => v <= ViewModels.Preferences.Instance.SubjectGuideLength); - - public static readonly FuncValueConverter ToTreeMargin = - new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); - - public static readonly FuncValueConverter ToBookmarkBrush = - new FuncValueConverter(bookmark => - { - if (bookmark == 0) - return Application.Current?.FindResource("Brush.FG1") as IBrush; - else - return Models.Bookmarks.Brushes[bookmark]; - }); } } diff --git a/src/Converters/InteractiveRebaseActionConverters.cs b/src/Converters/InteractiveRebaseActionConverters.cs deleted file mode 100644 index dbd183bd..00000000 --- a/src/Converters/InteractiveRebaseActionConverters.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace SourceGit.Converters -{ - public static class InteractiveRebaseActionConverters - { - public static readonly FuncValueConverter ToIconBrush = - new FuncValueConverter(v => - { - switch (v) - { - case Models.InteractiveRebaseAction.Pick: - return Brushes.Green; - case Models.InteractiveRebaseAction.Edit: - return Brushes.Orange; - case Models.InteractiveRebaseAction.Reword: - return Brushes.Orange; - case Models.InteractiveRebaseAction.Squash: - return Brushes.LightGray; - case Models.InteractiveRebaseAction.Fixup: - return Brushes.LightGray; - default: - return Brushes.Red; - } - }); - - public static readonly FuncValueConverter ToName = - new FuncValueConverter(v => - { - switch (v) - { - case Models.InteractiveRebaseAction.Pick: - return "Pick"; - case Models.InteractiveRebaseAction.Edit: - return "Edit"; - case Models.InteractiveRebaseAction.Reword: - return "Reword"; - case Models.InteractiveRebaseAction.Squash: - return "Squash"; - case Models.InteractiveRebaseAction.Fixup: - return "Fixup"; - default: - return "Drop"; - } - }); - - public static readonly FuncValueConverter CanEditMessage = - new FuncValueConverter(v => v == Models.InteractiveRebaseAction.Reword || v == Models.InteractiveRebaseAction.Squash); - } -} diff --git a/src/Converters/LauncherPageConverters.cs b/src/Converters/LauncherPageConverters.cs new file mode 100644 index 00000000..05eec2b1 --- /dev/null +++ b/src/Converters/LauncherPageConverters.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +using Avalonia.Collections; +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class LauncherPageConverters + { + public static readonly FuncMultiValueConverter ToTabSeperatorVisible = + new FuncMultiValueConverter(v => + { + if (v == null) + return false; + + var array = new List(); + array.AddRange(v); + if (array.Count != 3) + return false; + + var self = array[0] as ViewModels.LauncherPage; + if (self == null) + return false; + + var selected = array[1] as ViewModels.LauncherPage; + var collections = array[2] as AvaloniaList; + + if (selected != null && collections != null && (self == selected || collections.IndexOf(self) + 1 == collections.IndexOf(selected))) + { + return false; + } + else + { + return true; + } + }); + } +} diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index 6f3ae98b..dac55076 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.Collections.Generic; using Avalonia.Data.Converters; @@ -7,22 +6,10 @@ namespace SourceGit.Converters { public static class ListConverters { - public static readonly FuncValueConverter Count = - new FuncValueConverter(v => v == null ? "0" : $"{v.Count}"); - public static readonly FuncValueConverter ToCount = - new FuncValueConverter(v => v == null ? "(0)" : $"({v.Count})"); - - public static readonly FuncValueConverter IsNullOrEmpty = - new FuncValueConverter(v => v == null || v.Count == 0); + new FuncValueConverter(v => v == null ? " (0)" : $" ({v.Count})"); public static readonly FuncValueConverter IsNotNullOrEmpty = new FuncValueConverter(v => v != null && v.Count > 0); - - public static readonly FuncValueConverter, List> Top100Changes = - new FuncValueConverter, List>(v => (v == null || v.Count < 100) ? v : v.GetRange(0, 100)); - - public static readonly FuncValueConverter IsOnlyTop100Shows = - new FuncValueConverter(v => v != null && v.Count > 100); } } diff --git a/src/Converters/ObjectConverters.cs b/src/Converters/ObjectConverters.cs deleted file mode 100644 index f7c57764..00000000 --- a/src/Converters/ObjectConverters.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class ObjectConverters - { - public class IsTypeOfConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value == null || parameter == null) - return false; - - return value.GetType().IsAssignableTo((Type)parameter); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return new NotImplementedException(); - } - } - - public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter(); - } -} diff --git a/src/Converters/PathConverters.cs b/src/Converters/PathConverters.cs index ac1e61e5..6f10b66d 100644 --- a/src/Converters/PathConverters.cs +++ b/src/Converters/PathConverters.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using Avalonia.Data.Converters; @@ -8,23 +7,9 @@ namespace SourceGit.Converters public static class PathConverters { public static readonly FuncValueConverter PureFileName = - new(v => Path.GetFileName(v) ?? ""); + new FuncValueConverter(fullpath => Path.GetFileName(fullpath) ?? ""); public static readonly FuncValueConverter PureDirectoryName = - new(v => Path.GetDirectoryName(v) ?? ""); - - public static readonly FuncValueConverter RelativeToHome = - new(v => - { - if (OperatingSystem.IsWindows()) - return v; - - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; - if (v.StartsWith(home, StringComparison.Ordinal)) - return $"~{v.AsSpan(prefixLen)}"; - - return v; - }); + new FuncValueConverter(fullpath => Path.GetDirectoryName(fullpath) ?? ""); } } diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index bcadfae9..f743f69f 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -17,7 +17,7 @@ namespace SourceGit.Converters public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return (value as Models.Locale)?.Key; + return (value as Models.Locale).Key; } } @@ -28,21 +28,24 @@ namespace SourceGit.Converters public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var theme = (string)value; - if (string.IsNullOrEmpty(theme)) - return ThemeVariant.Default; - if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) + { return ThemeVariant.Light; - - if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) + } + else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) + { return ThemeVariant.Dark; - - return ThemeVariant.Default; + } + else + { + return ThemeVariant.Default; + } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return (value as ThemeVariant)?.Key; + var theme = (ThemeVariant)value; + return theme.Key; } } @@ -65,24 +68,6 @@ namespace SourceGit.Converters public static readonly FormatByResourceKeyConverter FormatByResourceKey = new FormatByResourceKeyConverter(); public static readonly FuncValueConverter ToShortSHA = - new FuncValueConverter(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); - - public static readonly FuncValueConverter TrimRefsPrefix = - new FuncValueConverter(v => - { - if (v == null) - return string.Empty; - if (v.StartsWith("refs/heads/", StringComparison.Ordinal)) - return v.Substring(11); - if (v.StartsWith("refs/remotes/", StringComparison.Ordinal)) - return v.Substring(13); - return v; - }); - - public static readonly FuncValueConverter ContainsSpaces = - new FuncValueConverter
Open-source GUI client for git users