Compare commits

..

No commits in common. "master" and "v6.0" have entirely different histories.
master ... v6.0

849 changed files with 23325 additions and 72166 deletions

View file

@ -1,306 +0,0 @@
# editorconfig.org
# top-most EditorConfig file
root = true
# Default settings:
# A newline ending every file
# Use 4 spaces as indentation
[*]
insert_final_newline = true
indent_style = space
indent_size = 4
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_prefer_collection_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
# C# files
[*.cs]
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# trim_trailing_whitespace = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
# avoid this. unless absolutely necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# prefer var
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true:suggestion
# use language keywords instead of BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# private static fields should have s_ prefix
dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion
dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
dotnet_naming_style.private_static_prefix_style.required_prefix = s_
dotnet_naming_style.private_static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
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
# use accessibility modifiers
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# Code style defaults
dotnet_sort_system_directives_first = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
# Null checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
space_within_single_line_array_initializer_braces = true
#Net Analyzer
dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed.
# CS0649: Field 'field' is never assigned to, and will always have its default value 'value'
dotnet_diagnostic.CS0649.severity = error
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = suggestion
# CS0162: Remove unreachable code
dotnet_diagnostic.CS0162.severity = error
# CA1018: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.CA1018.severity = error
# CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = error
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers
dotnet_diagnostic.CA1821.severity = warning
# CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = suggestion
# CA1823: Avoid unused private fields
dotnet_diagnostic.CA1823.severity = warning
dotnet_code_quality.CA1822.api_surface = private, internal
# CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning
# CA1826: Use property instead of Linq Enumerable method
dotnet_diagnostic.CA1826.severity = suggestion
# CA1827: Do not use Count/LongCount when Any can be used
dotnet_diagnostic.CA1827.severity = warning
# CA1828: Do not use CountAsync/LongCountAsync when AnyAsync can be used
dotnet_diagnostic.CA1828.severity = warning
# CA1829: Use Length/Count property instead of Enumerable.Count method
dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
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
# Avalonia DevAnalyzer preferences
dotnet_diagnostic.AVADEV2001.severity = error
# Avalonia PublicAnalyzer preferences
dotnet_diagnostic.AVP1000.severity = error
dotnet_diagnostic.AVP1001.severity = error
dotnet_diagnostic.AVP1002.severity = error
dotnet_diagnostic.AVP1010.severity = error
dotnet_diagnostic.AVP1011.severity = error
dotnet_diagnostic.AVP1012.severity = warning
dotnet_diagnostic.AVP1013.severity = error
dotnet_diagnostic.AVP1020.severity = error
dotnet_diagnostic.AVP1021.severity = error
dotnet_diagnostic.AVP1022.severity = error
dotnet_diagnostic.AVP1030.severity = error
dotnet_diagnostic.AVP1031.severity = error
dotnet_diagnostic.AVP1032.severity = error
dotnet_diagnostic.AVP1040.severity = error
dotnet_diagnostic.AVA2001.severity = error
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_style_prefer_readonly_struct = true:suggestion
csharp_prefer_static_local_function = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
# Xaml files
[*.{xaml,axaml}]
indent_size = 2
# DuplicateSetterError
avalonia_xaml_diagnostic.AVLN2203.severity = error
# StyleInMergedDictionaries
avalonia_xaml_diagnostic.AVLN2204.severity = error
# RequiredTemplatePartMissing
avalonia_xaml_diagnostic.AVLN2205.severity = error
# OptionalTemplatePartMissing
avalonia_xaml_diagnostic.AVLN2206.severity = info
# TemplatePartWrongType
avalonia_xaml_diagnostic.AVLN2207.severity = error
# Obsolete
avalonia_xaml_diagnostic.AVLN5001.severity = error
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
# Xml build files
[*.builds]
indent_size = 2
# Xml files
[*.{xml,stylecop,resx,ruleset}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
[*.json]
indent_size = 2
# Shell scripts
[*.sh]
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

14
.gitattributes vendored
View file

@ -1,14 +0,0 @@
* text=auto
*.md text
*.png binary
*.ico binary
*.sh text eol=lf
*.spec text eol=lf
control text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.json text
.gitattributes export-ignore
.gitignore export-ignore

View file

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

View file

@ -1,29 +0,0 @@
name: Continuous Integration
on:
push:
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 }}
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 }}

View file

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

View file

@ -1,111 +0,0 @@
name: Package
on:
workflow_call:
inputs:
version:
description: SourceGit package version
required: true
type: string
jobs:
windows:
name: Package Windows
runs-on: windows-2019
strategy:
matrix:
runtime: [ win-x64, win-arm64 ]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: build/SourceGit
- name: Package
shell: bash
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
run: ./build/scripts/package.windows.sh
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: build/sourcegit_*.zip
- name: Delete temp artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: sourcegit.${{ matrix.runtime }}
osx-app:
name: Package macOS
runs-on: macos-latest
strategy:
matrix:
runtime: [osx-x64, osx-arm64]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: build
- name: Package
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
run: |
mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
./build/scripts/package.osx-app.sh
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: build/sourcegit_*.zip
- name: Delete temp artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: sourcegit.${{ matrix.runtime }}
linux:
name: Package Linux
runs-on: ubuntu-latest
container: ubuntu:20.04
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Download package dependencies
run: |
export DEBIAN_FRONTEND=noninteractive
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
apt-get update
apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils
- name: Download build
uses: actions/download-artifact@v4
with:
name: sourcegit.${{ matrix.runtime }}
path: build
- name: Package
env:
VERSION: ${{ inputs.version }}
RUNTIME: ${{ matrix.runtime }}
APPIMAGE_EXTRACT_AND_RUN: 1
run: |
mkdir build/SourceGit
tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit
./build/scripts/package.linux.sh
- name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: |
build/sourcegit-*.AppImage
build/sourcegit_*.deb
build/sourcegit-*.rpm
- name: Delete temp artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: sourcegit.${{ matrix.runtime }}

View file

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

46
.gitignore vendored
View file

@ -1,41 +1,7 @@
.vs/ .idea
.vscode/ .vs
.idea/ .vscode
bin
*.sln.docstates obj
publish
*.user *.user
*.suo
*.code-workspace
.DS_Store
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
bin/
obj/
# ignore ci node files
node_modules/
package.json
package-lock.json
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

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2025 sourcegit Copyright (c) 2021 sourcegit
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

202
README.md
View file

@ -1,207 +1,55 @@
# SourceGit - Opensource Git GUI client. # SourceGit
[![stars](https://img.shields.io/github/stars/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/stargazers) Opensouce Git GUI client for Windows.
[![forks](https://img.shields.io/github/forks/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/forks)
[![license](https://img.shields.io/github/license/sourcegit-scm/sourcegit.svg)](LICENSE)
[![latest](https://img.shields.io/github/v/release/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/releases/latest)
[![downloads](https://img.shields.io/github/downloads/sourcegit-scm/sourcegit/total)](https://github.com/sourcegit-scm/sourcegit/releases)
## Highlights ## High-lights
* Supports Windows/macOS/Linux
* Opensource/Free * Opensource/Free
* Light-weight
* Fast * Fast
* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil) * English/简体中文
* Built-in light/dark themes * Build-in light/dark themes
* Customize theme
* Visual commit graph * Visual commit graph
* Supports SSH access with each remote * Supports SSH access with each remote
* GIT commands with GUI * GIT commands with GUI
* Clone/Fetch/Pull/Push... * Clone/Fetch/Pull/Push...
* Merge/Rebase/Reset/Revert/Cherry-pick...
* Amend/Reword/Squash
* Interactive rebase
* Branches * Branches
* Remotes * Remotes
* Tags * Tags
* Stashes * Stashes
* Submodules * Submodules
* Worktrees * Subtrees
* Archive * Archive
* Diff * Patch/apply
* Save as patch/apply
* File histories * File histories
* Blame * Blame
* Revision Diffs * Revision Diffs
* Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend
* Git command logs
* Search commits
* GitFlow
* Git LFS
* Bisect
* Issue Link
* Workspace
* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING] ## Download
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.
## Translation Status Pre-build Binaries[Releases](https://github.com/sourcegit-scm/sourcegit/releases)
You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md) | File | .NET runtime | Description |
| ------------------------ | ------------------ | --------------------------------- |
| SourceGit.exe | .NET 6 x64 | Need to be installed by user. |
| SourceGit.bundle.exe | Self-contained | - |
## How to Use > NOTE: You need install Git first.
**To use this tool, you need to install Git(>=2.25.1) first.** ## Screen Shots
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. * Drak Theme
This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs. ![Theme Dark](./screenshots/theme_dark.png)
| 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.
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)
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
```
* 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.
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
## 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 |
> [!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.
## Screenshots
* Dark Theme
![Theme Dark](./screenshots/theme_dark.png)
* Light Theme * Light Theme
![Theme Light](./screenshots/theme_light.png) ![Theme Light](./screenshots/theme_light.png)
* Custom ## Thanks
You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git). And welcome to share your own themes. * [XiaoLinger](https://gitee.com/LingerNN) Hotkey: `CTRL + Enter` to commit
* [carterl](https://gitee.com/carterl) Supports Windows Terminal; Rewrite way to find git executable
## Contributing * [PUMA](https://gitee.com/whgfu) Configure for default user
* [Rwing](https://gitee.com/rwing) GitFlow: add an option to keep branch after finish
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`. * [XiaoLinger](https://gitee.com/LingerNN) Fix localizations in popup panel
In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download):
```sh
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
dotnet restore
dotnet build
dotnet run --project src/SourceGit.csproj
```
Thanks to all the people who contribute.
[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=20)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors)
## Third-Party Components
For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md).

View file

@ -1,122 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34714.143
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{FD384607-ED99-47B7-AF31-FB245841BC92}"
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "files", "files", "{3AB707DB-A02C-4AFC-BF12-D7DF2B333BAC}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{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}
EndGlobalSection
EndGlobal

View file

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

View file

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

View file

@ -1 +0,0 @@
2025.22

9
build.bat Normal file
View file

@ -0,0 +1,9 @@
@echo off
rmdir /s /q publish
cd src
dotnet publish SourceGit.csproj --nologo -c Release -r win-x64 -f net6.0-windows -p:PublishSingleFile=true --no-self-contained -o ..\publish
dotnet publish SourceGit.csproj --nologo -c Release -r win-x64 -f net6.0-windows --self-contained -o ..\publish\SourceGit
cd ..

View file

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

View file

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIconFile</key>
<string>App.icns</string>
<key>CFBundleIdentifier</key>
<string>com.sourcegit-scm.sourcegit</string>
<key>CFBundleName</key>
<string>SourceGit</string>
<key>CFBundleVersion</key>
<string>SOURCE_GIT_VERSION.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>CFBundleExecutable</key>
<string>SourceGit</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>SOURCE_GIT_VERSION</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View file

@ -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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -1,70 +0,0 @@
#!/usr/bin/env bash
set -e
set -o
set -u
set pipefail
arch=
appimage_arch=
target=
case "$RUNTIME" in
linux-x64)
arch=amd64
appimage_arch=x86_64
target=x86_64;;
linux-arm64)
arch=arm64
appimage_arch=arm_aarch64
target=aarch64;;
*)
echo "Unknown runtime $RUNTIME"
exit 1;;
esac
APPIMAGETOOL_URL=https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
cd build
if [[ ! -f "appimagetool" ]]; then
curl -o appimagetool -L "$APPIMAGETOOL_URL"
chmod +x appimagetool
fi
rm -f SourceGit/*.dbg
mkdir -p SourceGit.AppDir/opt
mkdir -p SourceGit.AppDir/usr/share/metainfo
mkdir -p SourceGit.AppDir/usr/share/applications
cp -r SourceGit SourceGit.AppDir/opt/sourcegit
desktop-file-install resources/_common/applications/sourcegit.desktop --dir SourceGit.AppDir/usr/share/applications \
--set-icon com.sourcegit_scm.SourceGit --set-key=Exec --set-value=AppRun
mv SourceGit.AppDir/usr/share/applications/{sourcegit,com.sourcegit_scm.SourceGit}.desktop
cp resources/appimage/sourcegit.png SourceGit.AppDir/com.sourcegit_scm.SourceGit.png
ln -rsf SourceGit.AppDir/opt/sourcegit/sourcegit SourceGit.AppDir/AppRun
ln -rsf SourceGit.AppDir/usr/share/applications/com.sourcegit_scm.SourceGit.desktop SourceGit.AppDir
cp resources/appimage/sourcegit.appdata.xml SourceGit.AppDir/usr/share/metainfo/com.sourcegit_scm.SourceGit.appdata.xml
ARCH="$appimage_arch" ./appimagetool -v SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage"
mkdir -p resources/deb/opt/sourcegit/
mkdir -p resources/deb/usr/bin
mkdir -p resources/deb/usr/share/applications
mkdir -p resources/deb/usr/share/icons
cp -f SourceGit/* resources/deb/opt/sourcegit
ln -rsf resources/deb/opt/sourcegit/sourcegit resources/deb/usr/bin
cp -r resources/_common/applications resources/deb/usr/share
cp -r resources/_common/icons resources/deb/usr/share
# Calculate installed size in KB
installed_size=$(du -sk resources/deb | cut -f1)
# Update the control file
sed -i -e "s/^Version:.*/Version: $VERSION/" \
-e "s/^Architecture:.*/Architecture: $arch/" \
-e "s/^Installed-Size:.*/Installed-Size: $installed_size/" \
resources/deb/DEBIAN/control
# Build deb package with gzip compression
dpkg-deb -Zgzip --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Before After
Before After

View file

@ -1,58 +0,0 @@
using System;
using System.Windows.Input;
using Avalonia.Controls;
namespace SourceGit
{
public partial class App
{
public class Command : ICommand
{
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
public Command(Action<object> action)
{
_action = action;
}
public bool CanExecute(object parameter) => _action != null;
public void Execute(object parameter) => _action?.Invoke(parameter);
private Action<object> _action = null;
}
public static bool IsCheckForUpdateCommandVisible
{
get
{
#if DISABLE_UPDATE_DETECTION
return false;
#else
return true;
#endif
}
}
public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false));
public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false));
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false));
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true));
public static readonly Command QuitCommand = new Command(_ => Quit(0));
public static readonly Command CopyTextBlockCommand = new Command(p =>
{
var textBlock = p as TextBlock;
if (textBlock == null)
return;
if (textBlock.Inlines is { Count: > 0 } inlines)
CopyText(inlines.Text);
else if (!string.IsNullOrEmpty(textBlock.Text))
CopyText(textBlock.Text);
});
}
}

View file

@ -1,54 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Avalonia.Controls;
using Avalonia.Media;
namespace SourceGit
{
public class ColorConverter : JsonConverter<Color>
{
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<GridLength>
{
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))]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.RepositorySettings))]
[JsonSerializable(typeof(ViewModels.Preferences))]
internal partial class JsonCodeGen : JsonSerializerContext { }
}

View file

@ -1,47 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="using:SourceGit"
x:Class="SourceGit.App"
Name="SourceGit"
RequestedThemeVariant="Dark">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/Resources/Icons.axaml"/>
<ResourceInclude Source="/Resources/Themes.axaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceInclude x:Key="de_DE" Source="/Resources/Locales/de_DE.axaml"/>
<ResourceInclude x:Key="en_US" Source="/Resources/Locales/en_US.axaml"/>
<ResourceInclude x:Key="fr_FR" Source="/Resources/Locales/fr_FR.axaml"/>
<ResourceInclude x:Key="it_IT" Source="/Resources/Locales/it_IT.axaml"/>
<ResourceInclude x:Key="pt_BR" Source="/Resources/Locales/pt_BR.axaml"/>
<ResourceInclude x:Key="uk_UA" Source="/Resources/Locales/uk_UA.axaml"/>
<ResourceInclude x:Key="ru_RU" Source="/Resources/Locales/ru_RU.axaml"/>
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/>
<ResourceInclude x:Key="zh_TW" Source="/Resources/Locales/zh_TW.axaml"/>
<ResourceInclude x:Key="es_ES" Source="/Resources/Locales/es_ES.axaml"/>
<ResourceInclude x:Key="ja_JP" Source="/Resources/Locales/ja_JP.axaml"/>
<ResourceInclude x:Key="ta_IN" Source="/Resources/Locales/ta_IN.axaml"/>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="/Resources/Styles.axaml"/>
</Application.Styles>
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" Gesture="F1"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>
<NativeMenuItem Header="{DynamicResource Text.OpenAppDataDir}" Command="{x:Static s:App.OpenAppDataDirCommand}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Quit}" Command="{x:Static s:App.QuitCommand}" Gesture="⌘+Q"/>
</NativeMenu>
</NativeMenu.Menu>
</Application>

View file

@ -1,706 +0,0 @@
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;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
namespace SourceGit
{
public 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);
}
catch (Exception ex)
{
LogException(ex);
}
}
public static AppBuilder BuildAvaloniaApp()
{
var builder = AppBuilder.Configure<App>();
builder.UsePlatformDetect();
builder.LogToTrace();
builder.WithInterFont();
builder.With(new FontManagerOptions()
{
DefaultFamilyName = "fonts:Inter#Inter"
});
builder.ConfigureFonts(manager =>
{
var monospace = new EmbeddedFontCollection(
new Uri("fonts:SourceGit", UriKind.Absolute),
new Uri("avares://SourceGit/Resources/Fonts", UriKind.Absolute));
manager.AddFontCollection(monospace);
});
Native.OS.SetupApp(builder);
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);
}
public static void SendNotification(string context, string message)
{
if (Current is App app && app._launcher != null)
app._launcher.DispatchNotification(context, message, false);
}
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)
{
var app = Current as App;
if (app == null)
return;
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
app.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
}
}
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;
}
}
public static async void CopyText(string data)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow?.Clipboard is { } clipboard)
await clipboard.SetTextAsync(data ?? "");
}
}
public static async Task<string> GetClipboardTextAsync()
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow?.Clipboard is { } clipboard)
{
return await clipboard.GetTextAsync();
}
}
return null;
}
public static string Text(string key, params object[] args)
{
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);
}
public static Avalonia.Controls.Shapes.Path CreateMenuIcon(string key)
{
var icon = new Avalonia.Controls.Shapes.Path();
icon.Width = 12;
icon.Height = 12;
icon.Stretch = Stretch.Uniform;
if (Current?.FindResource(key) is StreamGeometry geo)
icon.Data = geo;
return icon;
}
public static IStorageProvider GetStorageProvider()
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
return desktop.MainWindow?.StorageProvider;
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<Control>((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<string>();
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)
{
Task.Run(async () =>
{
try
{
// Fetch latest release information.
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) };
var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json");
// Parse JSON into Models.Version.
var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version);
if (ver == null)
return;
// Check if already up-to-date.
if (!ver.IsNewVersion)
{
if (manually)
ShowSelfUpdateResult(new Models.AlreadyUpToDate());
return;
}
// Should not check ignored tag if this is called manually.
if (!manually)
{
var pref = ViewModels.Preferences.Instance;
if (ver.TagName == pref.IgnoreUpdateTag)
return;
}
ShowSelfUpdateResult(ver);
}
catch (Exception e)
{
if (manually)
ShowSelfUpdateResult(new Models.SelfUpdateFailed(e));
}
});
}
private void ShowSelfUpdateResult(object data)
{
Dispatcher.UIThread.Post(() =>
{
ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true);
});
}
private string FixFontFamilyName(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var parts = input.Split(',');
var trimmed = new List<string>();
foreach (var part in parts)
{
var t = part.Trim();
if (string.IsNullOrEmpty(t))
continue;
// Collapse multiple spaces into single space
var prevChar = '\0';
var sb = new StringBuilder();
foreach (var c in t)
{
if (c == ' ' && prevChar == ' ')
continue;
sb.Append(c);
prevChar = c;
}
var name = sb.ToString();
if (name.Contains('#', StringComparison.Ordinal))
{
if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) &&
!name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal))
continue;
}
trimmed.Add(name);
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
[GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")]
private static partial Regex REG_REBASE_TODO();
private Models.IpcChannel _ipcChannel = null;
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;
private ResourceDictionary _fontsOverrides = null;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

@ -1,18 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only. <dependency>
Don't remove it as it might cause problems with window transparency and embedded controls. <dependentAssembly>
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
<assemblyIdentity version="1.0.0.0" name="SourceGit.Desktop"/> </dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <application xmlns="urn:schemas-microsoft-com:asm.v3">
<application> <windowsSettings>
<!-- A list of the Windows versions that this application has been tested on <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
and is designed to work with. Uncomment the appropriate elements <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
and Windows will automatically select the most compatible environment. --> </windowsSettings>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application> </application>
</compatibility>
</assembly> </assembly>

14
src/App.xaml Normal file
View file

@ -0,0 +1,14 @@
<Application x:Class="SourceGit.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Icons.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Controls.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Themes/Light.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Locales/en_US.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

95
src/App.xaml.cs Normal file
View file

@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Windows;
namespace SourceGit {
/// <summary>
/// 程序入口.
/// </summary>
public partial class App : Application {
/// <summary>
/// 读取本地化字串
/// </summary>
/// <param name="key">本地化字串的Key</param>
/// <param name="args">可选格式化参数</param>
/// <returns>本地化字串</returns>
public static string Text(string key, params object[] args) {
var data = Current.FindResource($"Text.{key}") as string;
if (string.IsNullOrEmpty(data)) return $"Text.{key}";
return string.Format(data, args);
}
/// <summary>
/// 启动.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
// 崩溃上报
AppDomain.CurrentDomain.UnhandledException += (_, ev) => Models.Issue.Create(ev.ExceptionObject as Exception);
// 创建必要目录
if (!Directory.Exists(Views.Controls.Avatar.CACHE_PATH)) {
Directory.CreateDirectory(Views.Controls.Avatar.CACHE_PATH);
}
Models.Theme.Change();
Models.Locale.Change();
// 如果启动命令中指定了路径,打开指定目录的仓库
var launcher = new Views.Launcher();
if (Models.Preference.Instance.IsReady) {
if (e.Args.Length > 0) {
var repo = Models.Preference.Instance.FindRepository(e.Args[0]);
if (repo == null) {
var path = new Commands.GetRepositoryRootPath(e.Args[0]).Result();
if (path != null) {
var gitDir = new Commands.QueryGitDir(path).Result();
repo = Models.Preference.Instance.AddRepository(path, gitDir, "");
}
}
if (repo != null) Models.Watcher.Open(repo);
} else if (Models.Preference.Instance.Restore.IsEnabled) {
var restore = Models.Preference.Instance.Restore;
var actived = null as Models.Repository;
if (restore.Opened.Count > 0) {
foreach (var path in restore.Opened) {
if (!Directory.Exists(path)) continue;
var repo = Models.Preference.Instance.FindRepository(path);
if (repo != null) Models.Watcher.Open(repo);
if (path == restore.Actived) actived = repo;
}
if (actived != null) Models.Watcher.Open(actived);
}
}
}
// 主界面显示
MainWindow = launcher;
MainWindow.Show();
// 检测版本更新
Models.Version.Check(ver => Dispatcher.Invoke(() => {
var dialog = new Views.Upgrade(ver) { Owner = MainWindow };
dialog.ShowDialog();
}));
}
/// <summary>
/// 后台运行
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void OnDeactivated(EventArgs e) {
base.OnDeactivated(e);
GC.Collect();
Models.Preference.Save();
}
}
}

View file

@ -1,26 +1,27 @@
namespace SourceGit.Commands using System.Collections.Generic;
{ using System.Text;
public class Add : Command
{ namespace SourceGit.Commands {
public Add(string repo, bool includeUntracked) /// <summary>
{ /// `git add`命令
WorkingDirectory = repo; /// </summary>
Context = repo; public class Add : Command {
Args = includeUntracked ? "add ." : "add -u ."; public Add(string repo) {
Cwd = repo;
Args = "add .";
} }
public Add(string repo, Models.Change change) public Add(string repo, List<string> paths) {
{ StringBuilder builder = new StringBuilder();
WorkingDirectory = repo; builder.Append("add --");
Context = repo; foreach (var p in paths) {
Args = $"add -- \"{change.Path}\""; builder.Append(" \"");
} builder.Append(p);
builder.Append("\"");
}
public Add(string repo, string pathspecFromFile) Cwd = repo;
{ Args = builder.ToString();
WorkingDirectory = repo;
Context = repo;
Args = $"add --pathspec-from-file=\"{pathspecFromFile}\"";
} }
} }
} }

View file

@ -1,18 +1,14 @@
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public class Apply : Command /// 应用Patch
{ /// </summary>
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra) public class Apply : Command {
{
WorkingDirectory = repo; public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode) {
Context = repo; Cwd = repo;
Args = "apply "; Args = "apply ";
if (ignoreWhitespace) if (ignoreWhitespace) Args += "--ignore-whitespace ";
Args += "--ignore-whitespace "; else Args += $"--whitespace={whitespaceMode} ";
else
Args += $"--whitespace={whitespaceMode} ";
if (!string.IsNullOrEmpty(extra))
Args += $"{extra} ";
Args += $"\"{file}\""; Args += $"\"{file}\"";
} }
} }

View file

@ -1,12 +1,22 @@
namespace SourceGit.Commands using System;
{
public class Archive : Command namespace SourceGit.Commands {
{
public Archive(string repo, string revision, string saveTo) /// <summary>
{ /// 存档命令
WorkingDirectory = repo; /// </summary>
Context = repo; public class Archive : Command {
Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}"; private Action<string> handler;
public Archive(string repo, string revision, string to, Action<string> onProgress) {
Cwd = repo;
Args = $"archive --format=zip --verbose --output=\"{to}\" {revision}";
TraitErrorAsOutput = true;
handler = onProgress;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
} }
} }
} }

View file

@ -1,14 +0,0 @@
namespace SourceGit.Commands
{
public class AssumeUnchanged : Command
{
public AssumeUnchanged(string repo, string file, bool bAdd)
{
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
WorkingDirectory = repo;
Context = repo;
Args = $"update-index {mode} -- \"{file}\"";
}
}
}

View file

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

View file

@ -1,97 +1,58 @@
using System; using System;
using System.Text; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public partial class Blame : Command /// 逐行追溯
{ /// </summary>
[GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] public class Blame : Command {
private static partial Regex REG_FORMAT(); private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)");
private Data data = new Data();
public Blame(string repo, string file, string revision) public class Data {
{ public List<Models.BlameLine> Lines = new List<Models.BlameLine>();
WorkingDirectory = repo; public bool IsBinary = false;
Context = repo; }
public Blame(string repo, string file, string revision) {
Cwd = repo;
Args = $"blame -t {revision} -- \"{file}\""; Args = $"blame -t {revision} -- \"{file}\"";
RaiseError = false;
_result.File = file;
} }
public Models.BlameData Result() public Data Result() {
{ Exec();
var rs = ReadToEnd(); return data;
if (!rs.IsSuccess)
return _result;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
ParseLine(line);
if (_result.IsBinary)
break;
}
if (_needUnifyCommitSHA)
{
foreach (var line in _result.LineInfos)
{
if (line.CommitSHA.Length > _minSHALen)
{
line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen);
}
}
}
_result.Content = _content.ToString();
return _result;
} }
private void ParseLine(string line) public override void OnReadline(string line) {
{ if (data.IsBinary) return;
if (line.Contains('\0', StringComparison.Ordinal)) if (string.IsNullOrEmpty(line)) return;
{
_result.IsBinary = true; if (line.IndexOf('\0') >= 0) {
_result.LineInfos.Clear(); data.IsBinary = true;
data.Lines.Clear();
return; return;
} }
var match = REG_FORMAT().Match(line); var match = REG_FORMAT.Match(line);
if (!match.Success) if (!match.Success) return;
return;
_content.AppendLine(match.Groups[4].Value);
var commit = match.Groups[1].Value; var commit = match.Groups[1].Value;
var author = match.Groups[2].Value; var author = match.Groups[2].Value;
var timestamp = int.Parse(match.Groups[3].Value); var timestamp = int.Parse(match.Groups[3].Value);
var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString(_dateFormat); var content = match.Groups[4].Value;
var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
var info = new Models.BlameLineInfo() var blameLine = new Models.BlameLine() {
{ LineNumber = $"{data.Lines.Count + 1}",
IsFirstInGroup = commit != _lastSHA,
CommitSHA = commit, CommitSHA = commit,
Author = author, Author = author,
Time = when, Time = when,
Content = content,
}; };
_result.LineInfos.Add(info); data.Lines.Add(blameLine);
_lastSHA = commit;
if (line[0] == '^')
{
_needUnifyCommitSHA = true;
_minSHALen = Math.Min(_minSHALen, commit.Length);
}
} }
private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder();
private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly;
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;
} }
} }

View file

@ -1,83 +1,38 @@
using System.Text; namespace SourceGit.Commands {
/// <summary>
/// 分支相关操作
/// </summary>
class Branch : Command {
private string target = null;
namespace SourceGit.Commands public Branch(string repo, string branch) {
{ Cwd = repo;
public static class Branch target = branch;
{
public static string ShowCurrent(string repo)
{
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) public void Create(string basedOn) {
{ Args = $"branch {target} {basedOn}";
var builder = new StringBuilder(); Exec();
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;
return cmd.Exec();
} }
public static bool Rename(string repo, string name, string to, Models.ICommandLog log) public void Rename(string to) {
{ Args = $"branch -M {target} {to}";
var cmd = new Command(); Exec();
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 void SetUpstream(string upstream) {
{ Args = $"branch {target} ";
var cmd = new Command(); if (string.IsNullOrEmpty(upstream)) {
cmd.WorkingDirectory = repo; Args += "--unset-upstream";
cmd.Context = repo; } else {
cmd.Log = log; Args += $"-u {upstream}";
}
if (string.IsNullOrEmpty(upstream)) Exec();
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 void Delete() {
{ Args = $"branch -D {target}";
var cmd = new Command(); Exec();
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();
} }
} }
} }

81
src/Commands/Branches.cs Normal file
View file

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 解析所有的分支
/// </summary>
public class Branches : Command {
private static readonly string PREFIX_LOCAL = "refs/heads/";
private static readonly string PREFIX_REMOTE = "refs/remotes/";
private static readonly string CMD = "branch -l --all -v --format=\"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)\"";
private static readonly Regex REG_FORMAT = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
private static readonly Regex REG_AHEAD = new Regex(@"ahead (\d+)");
private static readonly Regex REG_BEHIND = new Regex(@"behind (\d+)");
private List<Models.Branch> loaded = new List<Models.Branch>();
public Branches(string path) {
Cwd = path;
Args = CMD;
}
public List<Models.Branch> Result() {
Exec();
return loaded;
}
public override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var branch = new Models.Branch();
var refName = match.Groups[1].Value;
if (refName.EndsWith("/HEAD")) return;
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) {
branch.Name = refName.Substring(PREFIX_LOCAL.Length);
branch.IsLocal = true;
} else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal)) {
var name = refName.Substring(PREFIX_REMOTE.Length);
var shortNameIdx = name.IndexOf('/');
if (shortNameIdx < 0) return;
branch.Remote = name.Substring(0, shortNameIdx);
branch.Name = name.Substring(branch.Remote.Length + 1);
branch.IsLocal = false;
} else {
branch.Name = refName;
branch.IsLocal = true;
}
branch.FullName = refName;
branch.Head = match.Groups[2].Value;
branch.IsCurrent = match.Groups[3].Value == "*";
branch.Upstream = match.Groups[4].Value;
branch.UpstreamTrackStatus = ParseTrackStatus(match.Groups[5].Value);
branch.HeadSubject = match.Groups[6].Value;
loaded.Add(branch);
}
private string ParseTrackStatus(string data) {
if (string.IsNullOrEmpty(data)) return "";
string track = "";
var ahead = REG_AHEAD.Match(data);
if (ahead.Success) {
track += ahead.Groups[1].Value + "↑ ";
}
var behind = REG_BEHIND.Match(data);
if (behind.Success) {
track += behind.Groups[1].Value + "↓";
}
return track.Trim();
}
}
}

View file

@ -1,56 +1,46 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public class Checkout : Command /// 检出
{ /// </summary>
public Checkout(string repo) public class Checkout : Command {
{ private Action<string> handler = null;
WorkingDirectory = repo;
Context = repo; public Checkout(string repo) {
Cwd = repo;
} }
public bool Branch(string branch, bool force) public bool Branch(string branch, Action<string> onProgress) {
{ Args = $"checkout --progress {branch}";
var builder = new StringBuilder(); TraitErrorAsOutput = true;
builder.Append("checkout --progress "); handler = onProgress;
if (force)
builder.Append("--force ");
builder.Append(branch);
Args = builder.ToString();
return Exec(); return Exec();
} }
public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite) public bool Branch(string branch, string basedOn, Action<string> onProgress) {
{ Args = $"checkout --progress -b {branch} {basedOn}";
var builder = new StringBuilder(); TraitErrorAsOutput = true;
builder.Append("checkout --progress "); handler = onProgress;
if (force)
builder.Append("--force ");
builder.Append(allowOverwrite ? "-B " : "-b ");
builder.Append(branch);
builder.Append(" ");
builder.Append(basedOn);
Args = builder.ToString();
return Exec(); return Exec();
} }
public bool Commit(string commitId, bool force) public bool File(string file, bool useTheirs) {
{ if (useTheirs) {
var option = force ? "--force" : string.Empty; Args = $"checkout --theirs -- \"{file}\"";
Args = $"checkout {option} --detach --progress {commitId}"; } else {
Args = $"checkout --ours -- \"{file}\"";
}
return Exec(); return Exec();
} }
public bool UseTheirs(List<string> files) public bool Files(List<string> files) {
{ StringBuilder builder = new StringBuilder();
var builder = new StringBuilder(); builder.Append("checkout -f -q --");
builder.Append("checkout --theirs --"); foreach (var f in files) {
foreach (var f in files)
{
builder.Append(" \""); builder.Append(" \"");
builder.Append(f); builder.Append(f);
builder.Append("\""); builder.Append("\"");
@ -59,24 +49,8 @@ namespace SourceGit.Commands
return Exec(); return Exec();
} }
public bool UseMine(List<string> files) public override void OnReadline(string line) {
{ handler?.Invoke(line);
var builder = new StringBuilder();
builder.Append("checkout --ours --");
foreach (var f in files)
{
builder.Append(" \"");
builder.Append(f);
builder.Append("\"");
}
Args = builder.ToString();
return Exec();
}
public bool FileWithRevision(string file, string revision)
{
Args = $"checkout --no-overlay {revision} -- \"{file}\"";
return Exec();
} }
} }
} }

View file

@ -1,20 +1,13 @@
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public class CherryPick : Command /// 遴选命令
{ /// </summary>
public CherryPick(string repo, string commits, bool noCommit, bool appendSourceToMessage, string extraParams) public class CherryPick : Command {
{
WorkingDirectory = repo;
Context = repo;
Args = "cherry-pick "; public CherryPick(string repo, string commit, bool noCommit) {
if (noCommit) var mode = noCommit ? "-n" : "--ff";
Args += "-n "; Cwd = repo;
if (appendSourceToMessage) Args = $"cherry-pick {mode} {commit}";
Args += "-x ";
if (!string.IsNullOrEmpty(extraParams))
Args += $"{extraParams} ";
Args += commits;
} }
} }
} }

View file

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

View file

@ -1,21 +1,34 @@
namespace SourceGit.Commands using System;
{
public class Clone : Command
{
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs)
{
Context = ctx;
WorkingDirectory = path;
SSHKey = sshKey;
Args = "clone --progress --verbose ";
if (!string.IsNullOrEmpty(extraArgs)) namespace SourceGit.Commands {
Args += $"{extraArgs} ";
/// <summary>
/// 克隆
/// </summary>
public class Clone : Command {
private Action<string> handler = null;
public Clone(string path, string url, string localName, string sshKey, string extraArgs, Action<string> outputHandler) {
Cwd = path;
TraitErrorAsOutput = true;
handler = outputHandler;
if (!string.IsNullOrEmpty(sshKey)) {
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'");
Args = "";
} else {
Args = "-c credential.helper=manager-core ";
}
Args += "clone --progress --verbose --recurse-submodules ";
if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} ";
Args += $"{url} "; Args += $"{url} ";
if (!string.IsNullOrEmpty(localName)) Args += localName;
}
if (!string.IsNullOrEmpty(localName)) public override void OnReadline(string line) {
Args += localName; handler?.Invoke(line);
} }
} }
} }

View file

@ -3,74 +3,116 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using Avalonia.Threading; namespace SourceGit.Commands {
namespace SourceGit.Commands /// <summary>
{ /// 用于取消命令执行的上下文对象
public partial class Command /// </summary>
{ public class Context {
public class ReadToEndResult public bool IsCancelRequested { get; set; } = false;
{ }
public bool IsSuccess { get; set; } = false;
public string StdOut { get; set; } = ""; /// <summary>
public string StdErr { get; set; } = ""; /// 命令接口
/// </summary>
public class Command {
/// <summary>
/// 读取全部输出时的结果
/// </summary>
public class ReadToEndResult {
public bool IsSuccess { get; set; }
public string Output { get; set; }
public string Error { get; set; }
} }
public enum EditorType /// <summary>
{ /// 上下文
None, /// </summary>
CoreEditor, public Context Ctx { get; set; } = null;
RebaseEditor,
}
public string Context { get; set; } = string.Empty; /// <summary>
public CancellationToken CancellationToken { get; set; } = CancellationToken.None; /// 运行路径
public string WorkingDirectory { get; set; } = null; /// </summary>
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode public string Cwd { get; set; } = "";
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 Exec() /// <summary>
{ /// 参数
Log?.AppendLine($"$ git {Args}\n"); /// </summary>
public string Args { get; set; } = "";
var start = CreateGitStartInfo(); /// <summary>
/// 是否忽略错误
/// </summary>
public bool DontRaiseError { get; set; } = false;
/// <summary>
/// 使用标准错误输出
/// </summary>
public bool TraitErrorAsOutput { get; set; } = false;
/// <summary>
/// 用于设置该进程独有的环境变量
/// </summary>
public Dictionary<string, string> Envs { get; set; } = new Dictionary<string, string>();
/// <summary>
/// 运行
/// </summary>
public bool Exec() {
var start = new ProcessStartInfo();
start.FileName = Models.Preference.Instance.Git.Path;
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(Cwd)) start.WorkingDirectory = Cwd;
foreach (var kv in Envs) start.EnvironmentVariables[kv.Key] = kv.Value;
var progressFilter = new Regex(@"\s\d+%\s");
var errs = new List<string>(); var errs = new List<string>();
var proc = new Process() { StartInfo = start }; var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); proc.OutputDataReceived += (o, e) => {
proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs); if (Ctx != null && Ctx.IsCancelRequested) {
isCancelled = true;
var dummy = null as Process; proc.CancelErrorRead();
var dummyProcLock = new object(); proc.CancelOutputRead();
try if (!proc.HasExited) proc.Kill(true);
{ return;
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); if (e.Data == null) return;
OnReadline(e.Data);
};
proc.ErrorDataReceived += (o, e) => {
if (Ctx != null && Ctx.IsCancelRequested) {
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited) proc.Kill(true);
return;
}
if (string.IsNullOrEmpty(e.Data)) return;
if (TraitErrorAsOutput) OnReadline(e.Data);
if (progressFilter.IsMatch(e.Data)) return;
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return;
errs.Add(e.Data);
};
try {
proc.Start();
} catch (Exception e) {
if (!DontRaiseError) Models.Exception.Raise(e.Message);
return false; return false;
} }
@ -78,57 +120,47 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine(); proc.BeginErrorReadLine();
proc.WaitForExit(); proc.WaitForExit();
if (dummy != null)
{
lock (dummyProcLock)
{
dummy = null;
}
}
int exitCode = proc.ExitCode; int exitCode = proc.ExitCode;
proc.Close(); proc.Close();
Log?.AppendLine(string.Empty);
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
if (RaiseError)
{
var errMsg = string.Join("\n", errs).Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg));
}
if (!isCancelled && exitCode != 0 && errs.Count > 0) {
if (!DontRaiseError) Models.Exception.Raise(string.Join("\n", errs));
return false; return false;
} else {
return true;
} }
return true;
} }
public ReadToEndResult ReadToEnd() /// <summary>
{ /// 直接读取全部标准输出
var start = CreateGitStartInfo(); /// </summary>
var proc = new Process() { StartInfo = start }; public ReadToEndResult ReadToEnd() {
var start = new ProcessStartInfo();
start.FileName = Models.Preference.Instance.Git.Path;
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;
try if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
{
var proc = new Process() { StartInfo = start };
try {
proc.Start(); proc.Start();
} } catch (Exception e) {
catch (Exception e) return new ReadToEndResult() {
{ Output = "",
return new ReadToEndResult() Error = e.Message,
{
IsSuccess = false, IsSuccess = false,
StdOut = string.Empty,
StdErr = e.Message,
}; };
} }
var rs = new ReadToEndResult() var rs = new ReadToEndResult();
{ rs.Output = proc.StandardOutput.ReadToEnd();
StdOut = proc.StandardOutput.ReadToEnd(), rs.Error = proc.StandardError.ReadToEnd();
StdErr = proc.StandardError.ReadToEnd(),
};
proc.WaitForExit(); proc.WaitForExit();
rs.IsSuccess = proc.ExitCode == 0; rs.IsSuccess = proc.ExitCode == 0;
@ -137,84 +169,10 @@ namespace SourceGit.Commands
return rs; return rs;
} }
private ProcessStartInfo CreateGitStartInfo() /// <summary>
{ /// 调用Exec时的读取函数
var start = new ProcessStartInfo(); /// </summary>
start.FileName = Native.OS.GitExecutable; /// <param name="line"></param>
start.Arguments = "--no-pager -c core.quotepath=off -c credential.helper=manager "; public virtual void OnReadline(string line) { }
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<string> errs)
{
line ??= string.Empty;
Log?.AppendLine(line);
// Lines to hide in error message.
if (line.Length > 0)
{
if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) ||
line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) ||
line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) ||
line.StartsWith("Filtering content:", StringComparison.Ordinal) ||
line.StartsWith("hint:", StringComparison.Ordinal))
return;
if (REG_PROGRESS().IsMatch(line))
return;
}
errs.Add(line);
}
[GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS();
} }
} }

View file

@ -1,39 +1,17 @@
using System.IO; using System.IO;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public class Commit : Command /// `git commit`命令
{ /// </summary>
public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor) public class Commit : Command {
{ public Commit(string repo, string message, bool amend) {
_tmpFile = Path.GetTempFileName(); var file = Path.GetTempFileName();
File.WriteAllText(_tmpFile, message); File.WriteAllText(file, message);
WorkingDirectory = repo; Cwd = repo;
Context = repo; Args = $"commit --file=\"{file}\"";
Args = $"commit --allow-empty --file=\"{_tmpFile}\""; if (amend) Args += " --amend --no-edit";
if (signOff)
Args += " --signoff";
if (amend)
Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit";
} }
public bool Run()
{
var succ = Exec();
try
{
File.Delete(_tmpFile);
}
catch
{
// Ignore
}
return succ;
}
private readonly string _tmpFile;
} }
} }

View file

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 取得一个提交的变更列表
/// </summary>
public class CommitChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private List<Models.Change> changes = new List<Models.Change>();
public CommitChanges(string cwd, string commit) {
Cwd = cwd;
Args = $"show --name-status {commit}";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public 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.Change.Status.Modified); changes.Add(change); break;
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
}
}
}
}

View file

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 对比两个提交间的变更
/// </summary>
public class CommitRangeChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private List<Models.Change> changes = new List<Models.Change>();
public CommitRangeChanges(string cwd, string start, string end) {
Cwd = cwd;
Args = $"diff --name-status {start} {end}";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public 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.Change.Status.Modified); changes.Add(change); break;
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
}
}
}
}

149
src/Commands/Commits.cs Normal file
View file

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SourceGit.Commands {
/// <summary>
/// 取得提交列表
/// </summary>
public class Commits : Command {
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
private List<Models.Commit> commits = new List<Models.Commit>();
private Models.Commit current = null;
private bool isSkipingGpgsig = false;
private bool isHeadFounded = false;
private bool findFirstMerged = true;
public Commits(string path, string limits, bool needFindHead = true) {
Cwd = path;
Args = "log --date-order --decorate=full --pretty=raw " + limits;
findFirstMerged = needFindHead;
}
public List<Models.Commit> Result() {
Exec();
if (current != null) {
current.Message = current.Message.Trim();
commits.Add(current);
}
if (findFirstMerged && !isHeadFounded && commits.Count > 0) {
MarkFirstMerged();
}
return commits;
}
public override void OnReadline(string line) {
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('(');
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;
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)) {
current.Author.Parse(line);
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) {
current.Committer.Parse(line);
} else if (string.IsNullOrEmpty(current.Subject)) {
current.Subject = line.Trim();
} else {
current.Message += (line.Trim() + "\n");
}
}
private bool ParseDecorators(List<Models.Decorator> 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.Last().Committer.Time}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.Output.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0) return;
var set = new HashSet<string>();
foreach (var sha in shas) set.Add(sha);
foreach (var c in commits) {
if (set.Contains(c.SHA)) {
c.IsMerged = true;
break;
}
}
}
}
}

View file

@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class CompareRevisions : Command
{
[GeneratedRegex(@"^([MADC])\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}\"";
}
public List<Models.Change> Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
return _changes;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
ParseLine(line);
_changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path));
return _changes;
}
private void ParseLine(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;
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 'C':
change.Set(Models.ChangeState.Copied);
_changes.Add(change);
break;
}
}
private readonly List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -1,68 +1,36 @@
using System; namespace SourceGit.Commands {
using System.Collections.Generic; /// <summary>
/// config命令
/// </summary>
public class Config : Command {
namespace SourceGit.Commands public Config() { }
{
public class Config : Command
{
public Config(string repository)
{
if (string.IsNullOrEmpty(repository))
{
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
else
{
WorkingDirectory = repository;
Context = repository;
_isLocal = true;
}
RaiseError = false; public Config(string repo) {
Cwd = repo;
} }
public Dictionary<string, string> ListAll() public string Get(string key) {
{ Args = $"config {key}";
Args = "config -l"; return ReadToEnd().Output.Trim();
}
var output = ReadToEnd(); public bool Set(string key, string val, bool allowEmpty = false) {
var rs = new Dictionary<string, string>(); if (!allowEmpty && string.IsNullOrEmpty(val)) {
if (output.IsSuccess) if (string.IsNullOrEmpty(Cwd)) {
{ Args = $"config --global --unset {key}";
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); } else {
foreach (var line in lines) Args = $"config --unset {key}";
{ }
var idx = line.IndexOf('=', StringComparison.Ordinal); } else {
if (idx != -1) if (string.IsNullOrEmpty(Cwd)) {
{ Args = $"config --global {key} \"{val}\"";
var key = line.Substring(0, idx).Trim(); } else {
var val = line.Substring(idx + 1).Trim(); Args = $"config {key} \"{val}\"";
rs[key] = val;
}
} }
} }
return rs;
}
public string Get(string key)
{
Args = $"config {key}";
return ReadToEnd().StdOut.Trim();
}
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}";
else
Args = $"config {scope} {key} \"{value}\"";
return Exec(); return Exec();
} }
private bool _isLocal = false;
} }
} }

View file

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

View file

@ -1,281 +1,111 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public partial class Diff : Command /// Diff命令用于文件文件比对
{ /// </summary>
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] public class Diff : Command {
private static partial Regex REG_INDICATOR(); private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
private Models.TextChanges changes = new Models.TextChanges();
private List<Models.TextChanges.Line> deleted = new List<Models.TextChanges.Line>();
private List<Models.TextChanges.Line> added = new List<Models.TextChanges.Line>();
private int oldLine = 0;
private int newLine = 0;
private int lineIndex = 0;
[GeneratedRegex(@"^index\s([0-9a-f]{6,40})\.\.([0-9a-f]{6,40})(\s[1-9]{6})?")] public Diff(string repo, string args) {
private static partial Regex REG_HASH_CHANGE(); Cwd = repo;
Args = $"diff --ignore-cr-at-eol --unified=4 {args}";
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)
{
_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}";
} }
public Models.DiffResult Result() public Models.TextChanges Result() {
{ Exec();
var rs = ReadToEnd(); ProcessChanges();
var start = 0; if (changes.IsBinary) changes.Lines.Clear();
var end = rs.StdOut.IndexOf('\n', start); lineIndex = 0;
while (end > 0) return changes;
{
var line = rs.StdOut.Substring(start, end - start);
ParseLine(line);
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)
{
_result.TextDiff = null;
}
else
{
ProcessInlineHighlights();
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
return _result;
} }
private void ParseLine(string line) public override void OnReadline(string line) {
{ if (changes.IsBinary) return;
if (_result.IsBinary)
return;
if (line.StartsWith("old mode ", StringComparison.Ordinal)) if (changes.Lines.Count == 0) {
{ var match = REG_INDICATOR.Match(line);
_result.OldMode = line.Substring(9); if (!match.Success) {
return; if (line.StartsWith("Binary", StringComparison.Ordinal)) changes.IsBinary = true;
}
if (line.StartsWith("new mode ", StringComparison.Ordinal))
{
_result.NewMode = line.Substring(9);
return;
}
if (line.StartsWith("deleted file mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(18);
return;
}
if (line.StartsWith("new file mode ", StringComparison.Ordinal))
{
_result.NewMode = line.Substring(14);
return;
}
if (_result.IsLFS)
{
var ch = line[0];
if (ch == '-')
{
if (line.StartsWith("-oid sha256:", StringComparison.Ordinal))
{
_result.LFSDiff.Old.Oid = line.Substring(12);
}
else if (line.StartsWith("-size ", StringComparison.Ordinal))
{
_result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
}
}
else if (ch == '+')
{
if (line.StartsWith("+oid sha256:", StringComparison.Ordinal))
{
_result.LFSDiff.New.Oid = line.Substring(12);
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = long.Parse(line.AsSpan(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
}
return;
}
if (_result.TextDiff.Lines.Count == 0)
{
if (line.StartsWith("Binary", StringComparison.Ordinal))
{
_result.IsBinary = true;
return; return;
} }
if (string.IsNullOrEmpty(_result.OldHash)) oldLine = int.Parse(match.Groups[1].Value);
{ newLine = int.Parse(match.Groups[2].Value);
var match = REG_HASH_CHANGE().Match(line); changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Indicator, line, "", ""));
if (!match.Success) } else {
return; if (line.Length == 0) {
ProcessChanges();
_result.OldHash = match.Groups[1].Value; changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Normal, "", $"{oldLine}", $"{newLine}"));
_result.NewHash = match.Groups[2].Value; oldLine++;
} newLine++;
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);
}
}
else
{
if (line.Length == 0)
{
ProcessInlineHighlights();
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
return; return;
} }
var ch = line[0]; var ch = line[0];
if (ch == '-') if (ch == '-') {
{ deleted.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", ""));
if (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal)) oldLine++;
{ } else if (ch == '+') {
_result.IsLFS = true; added.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}"));
_result.LFSDiff = new Models.LFSDiff(); newLine++;
return; } else if (ch != '\\') {
ProcessChanges();
var match = REG_INDICATOR.Match(line);
if (match.Success) {
oldLine = int.Parse(match.Groups[1].Value);
newLine = int.Parse(match.Groups[2].Value);
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Indicator, line, "", ""));
} else {
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}"));
oldLine++;
newLine++;
} }
_last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0);
_deleted.Add(_last);
_oldLine++;
}
else if (ch == '+')
{
if (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal))
{
_result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff();
return;
}
_last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine);
_added.Add(_last);
_newLine++;
}
else if (ch != '\\')
{
ProcessInlineHighlights();
var match = REG_INDICATOR().Match(line);
if (match.Success)
{
_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);
}
else
{
if (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal))
{
_result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff();
return;
}
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
}
}
else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal))
{
_last.NoNewLineEndOfFile = true;
} }
} }
} }
private void ProcessInlineHighlights() private void ProcessChanges() {
{ if (deleted.Any()) {
if (_deleted.Count > 0) if (added.Count == deleted.Count) {
{ for (int i = added.Count - 1; i >= 0; i--) {
if (_added.Count == _deleted.Count) var left = deleted[i];
{ var right = added[i];
for (int i = _added.Count - 1; i >= 0; i--)
{
var left = _deleted[i];
var right = _added[i];
if (left.Content.Length > 1024 || right.Content.Length > 1024) if (left.Content.Length > 1024 || right.Content.Length > 1024) continue;
continue;
var chunks = Models.TextInlineChange.Compare(left.Content, right.Content); var chunks = Models.TextCompare.Process(left.Content, right.Content);
if (chunks.Count > 4) if (chunks.Count > 4) continue;
continue;
foreach (var chunk in chunks) foreach (var chunk in chunks) {
{ if (chunk.DeletedCount > 0) {
if (chunk.DeletedCount > 0) left.Highlights.Add(new Models.TextChanges.HighlightRange(chunk.DeletedStart, chunk.DeletedCount));
{
left.Highlights.Add(new Models.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount));
} }
if (chunk.AddedCount > 0) if (chunk.AddedCount > 0) {
{ right.Highlights.Add(new Models.TextChanges.HighlightRange(chunk.AddedStart, chunk.AddedCount));
right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount));
} }
} }
} }
} }
_result.TextDiff.Lines.AddRange(_deleted); changes.Lines.AddRange(deleted);
_deleted.Clear(); deleted.Clear();
} }
if (_added.Count > 0) if (added.Any()) {
{ changes.Lines.AddRange(added);
_result.TextDiff.Lines.AddRange(_added); added.Clear();
_added.Clear();
} }
} }
private readonly Models.DiffResult _result = new Models.DiffResult();
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private Models.TextDiffLine _last = null;
private int _oldLine = 0;
private int _newLine = 0;
} }
} }

View file

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

View file

@ -1,86 +0,0 @@
using System;
using System.Diagnostics;
using System.Text;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class ExecuteCustomAction
{
public static void Run(string repo, string file, string args)
{
var start = new ProcessStartInfo();
start.FileName = file;
start.Arguments = args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.WorkingDirectory = repo;
try
{
Process.Start(start);
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
}
public static void RunAndWait(string repo, string file, string args, Models.ICommandLog log)
{
var start = new ProcessStartInfo();
start.FileName = file;
start.Arguments = args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo;
log?.AppendLine($"$ {file} {args}\n");
var proc = new Process() { StartInfo = start };
var builder = new StringBuilder();
proc.OutputDataReceived += (_, e) =>
{
if (e.Data != null)
log?.AppendLine(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (e.Data != null)
{
log?.AppendLine(e.Data);
builder.AppendLine(e.Data);
}
};
try
{
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
proc.WaitForExit();
var exitCode = proc.ExitCode;
if (exitCode != 0)
{
var errMsg = builder.ToString().Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, errMsg));
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
proc.Close();
}
}
}

View file

@ -1,31 +1,88 @@
namespace SourceGit.Commands using System;
{ using System.Collections.Generic;
public class Fetch : Command using System.Threading;
{
public Fetch(string repo, string remote, bool noTags, bool force)
{
WorkingDirectory = repo;
Context = repo;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "fetch --progress --verbose ";
if (noTags) namespace SourceGit.Commands {
Args += "--no-tags ";
else
Args += "--tags ";
if (force) /// <summary>
Args += "--force "; /// 拉取
/// </summary>
public class Fetch : Command {
private Action<string> handler = null;
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler) {
Cwd = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'");
Args = "";
} else {
Args = "-c credential.helper=manager-core ";
}
Args += "fetch --progress --verbose ";
if (prune) Args += "--prune ";
Args += remote; Args += remote;
handler = outputHandler;
AutoFetch.MarkFetched(repo);
} }
public Fetch(string repo, Models.Branch local, Models.Branch remote) public override void OnReadline(string line) {
{ handler?.Invoke(line);
WorkingDirectory = repo; }
Context = repo; }
SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey");
Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}"; /// <summary>
/// 自动拉取每隔10分钟
/// </summary>
public class AutoFetch {
private static Dictionary<string, AutoFetch> jobs = new Dictionary<string, AutoFetch>();
private Fetch cmd = null;
private long nextFetchPoint = 0;
private Timer timer = null;
public static void Start(string repo) {
if (!Models.Preference.Instance.Git.AutoFetchRemotes) return;
// 只自动更新加入管理列表中的仓库(子模块等不自动更新)
var exists = Models.Preference.Instance.FindRepository(repo);
if (exists == null) return;
var job = new AutoFetch(repo);
jobs.Add(repo, job);
}
public static void MarkFetched(string repo) {
if (!jobs.ContainsKey(repo)) return;
jobs[repo].nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
}
public static void Stop(string repo) {
if (!jobs.ContainsKey(repo)) return;
jobs[repo].timer.Dispose();
jobs.Remove(repo);
}
public AutoFetch(string repo) {
cmd = new Fetch(repo, "--all", true, null);
cmd.DontRaiseError = true;
nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
timer = new Timer(OnTick, null, 60000, 10000);
}
private void OnTick(object o) {
var now = DateTime.Now.ToFileTime();
if (nextFetchPoint > now) return;
Models.Watcher.SetEnabled(cmd.Cwd, false);
cmd.Exec();
nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
Models.Watcher.SetEnabled(cmd.Cwd, true);
} }
} }
} }

View file

@ -1,13 +1,12 @@
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public class FormatPatch : Command /// 将Commit另存为Patch文件
{ /// </summary>
public FormatPatch(string repo, string commit, string saveTo) public class FormatPatch : Command {
{
WorkingDirectory = repo; public FormatPatch(string repo, string commit, string path) {
Context = repo; Cwd = repo;
Editor = EditorType.None; Args = $"format-patch {commit} -1 -o \"{path}\"";
Args = $"format-patch {commit} -1 --output=\"{saveTo}\"";
} }
} }
} }

View file

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

View file

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

View file

@ -0,0 +1,17 @@
namespace SourceGit.Commands {
/// <summary>
/// 取得一个库的根路径
/// </summary>
public class GetRepositoryRootPath : Command {
public GetRepositoryRootPath(string path) {
Cwd = path;
Args = "rev-parse --show-toplevel";
}
public string Result() {
var rs = ReadToEnd().Output;
if (string.IsNullOrEmpty(rs)) return null;
return rs.Trim();
}
}
}

View file

@ -1,92 +1,72 @@
using System.Text; namespace SourceGit.Commands {
using Avalonia.Threading; /// <summary>
/// Git-Flow命令
/// </summary>
public class GitFlow : Command {
namespace SourceGit.Commands public GitFlow(string repo) {
{ Cwd = repo;
public static class GitFlow
{
public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log)
{
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();
} }
public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) public bool Init(string master, string develop, string feature, string release, string hotfix, string version) {
{ var branches = new Branches(Cwd).Result();
var start = new Command(); var current = branches.Find(x => x.IsCurrent);
start.WorkingDirectory = repo;
start.Context = repo;
switch (type) var masterBranch = branches.Find(x => x.Name == master);
{ if (masterBranch == null && current != null) new Branch(Cwd, develop).Create(current.Head);
case Models.GitFlowBranchType.Feature:
start.Args = $"flow feature start {name}";
break;
case Models.GitFlowBranchType.Release:
start.Args = $"flow release start {name}";
break;
case Models.GitFlowBranchType.Hotfix:
start.Args = $"flow hotfix start {name}";
break;
default:
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
return false;
}
start.Log = log; var devBranch = branches.Find(x => x.Name == develop);
return start.Exec(); if (devBranch == null && current != null) new Branch(Cwd, develop).Create(current.Head);
var cmd = new Config(Cwd);
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 static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) public void Start(Models.GitFlowBranchType type, string name) {
{ switch (type) {
var builder = new StringBuilder(); case Models.GitFlowBranchType.Feature:
builder.Append("flow "); Args = $"flow feature start {name}";
break;
switch (type) case Models.GitFlowBranchType.Release:
{ Args = $"flow release start {name}";
case Models.GitFlowBranchType.Feature: break;
builder.Append("feature"); case Models.GitFlowBranchType.Hotfix:
break; Args = $"flow hotfix start {name}";
case Models.GitFlowBranchType.Release: break;
builder.Append("release"); default:
break; return;
case Models.GitFlowBranchType.Hotfix:
builder.Append("hotfix");
break;
default:
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
return false;
} }
builder.Append(" finish "); Exec();
if (squash) }
builder.Append("--squash ");
if (push)
builder.Append("--push ");
if (keepBranch)
builder.Append("-k ");
builder.Append(name);
var finish = new Command(); public void Finish(Models.GitFlowBranchType type, string name, bool keepBranch) {
finish.WorkingDirectory = repo; var option = keepBranch ? "-k" : string.Empty;
finish.Context = repo; switch (type) {
finish.Args = builder.ToString(); case Models.GitFlowBranchType.Feature:
finish.Log = log; Args = $"flow feature finish {option} {name}";
return finish.Exec(); break;
case Models.GitFlowBranchType.Release:
Args = $"flow release finish {option} {name} -m \"RELEASE_DONE\"";
break;
case Models.GitFlowBranchType.Hotfix:
Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\"";
break;
default:
return;
}
Exec();
} }
} }
} }

View file

@ -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]);
}
}
}

View file

@ -1,11 +1,12 @@
namespace SourceGit.Commands namespace SourceGit.Commands {
{
public class Init : Command /// <summary>
{ /// 初始化Git仓库
public Init(string ctx, string dir) /// </summary>
{ public class Init : Command {
Context = ctx;
WorkingDirectory = dir; public Init(string workDir) {
Cwd = workDir;
Args = "init -q"; Args = "init -q";
} }
} }

View file

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

View file

@ -1,23 +0,0 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class IsBinary : Command
{
[GeneratedRegex(@"^\-\s+\-\s+.*$")]
private static partial Regex REG_TEST();
public IsBinary(string repo, string commit, string path)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- \"{path}\"";
RaiseError = false;
}
public bool Result()
{
return REG_TEST().IsMatch(ReadToEnd().StdOut);
}
}
}

View file

@ -0,0 +1,18 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 查询指定版本下的某文件是否是二进制文件
/// </summary>
public class IsBinaryFile : Command {
private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$");
public IsBinaryFile(string repo, string commit, string path) {
Cwd = repo;
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
}
public bool Result() {
return REG_TEST.IsMatch(ReadToEnd().Output);
}
}
}

View file

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

View file

@ -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;
}
}
}

View file

@ -1,27 +0,0 @@
namespace SourceGit.Commands
{
public class IsLFSFiltered : Command
{
public IsLFSFiltered(string repo, string path)
{
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}\"";
RaiseError = false;
}
public bool Result()
{
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Contains("filter\0lfs");
}
}
}

View file

@ -1,115 +1,51 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public partial class LFS /// LFS相关
{ /// </summary>
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] public class LFS {
private static partial Regex REG_LOCK(); private string repo;
private class SubCmd : Command private class PruneCmd : Command {
{ private Action<string> handler;
public SubCmd(string repo, string args, Models.ICommandLog log)
{ public PruneCmd(string repo, Action<string> onProgress) {
WorkingDirectory = repo; Cwd = repo;
Context = repo; Args = "lfs prune";
Args = args; TraitErrorAsOutput = true;
Log = log; handler = onProgress;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
} }
} }
public LFS(string repo) public LFS(string repo) {
{ this.repo = repo;
_repo = repo;
} }
public bool IsEnabled() public bool IsEnabled() {
{ var path = Path.Combine(repo, ".git", "hooks", "pre-push");
var path = Path.Combine(_repo, ".git", "hooks", "pre-push"); if (!File.Exists(path)) return false;
if (!File.Exists(path))
return false;
var content = File.ReadAllText(path); var content = File.ReadAllText(path);
return content.Contains("git lfs pre-push"); return content.Contains("git lfs pre-push");
} }
public bool Install(Models.ICommandLog log) public bool IsFiltered(string path) {
{ var cmd = new Command();
return new SubCmd(_repo, "lfs install --local", log).Exec(); cmd.Cwd = repo;
} cmd.Args = $"check-attr -a -z \"{path}\"";
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<Models.LFSLock> Locks(string remote)
{
var locks = new List<Models.LFSLock>();
var cmd = new SubCmd(_repo, $"lfs locks --remote={remote}", null);
var rs = cmd.ReadToEnd(); var rs = cmd.ReadToEnd();
if (rs.IsSuccess) return rs.Output.Contains("filter\0lfs");
{
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) public void Prune(Action<string> onProgress) {
{ new PruneCmd(repo, onProgress).Exec();
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();
}
private readonly string _repo;
} }
} }

View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 取得本地工作副本变更
/// </summary>
public class LocalChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private static readonly string[] UNTRACKED = new string[] { "no", "all" };
private List<Models.Change> changes = new List<Models.Change>();
public LocalChanges(string path, bool includeUntracked = true) {
Cwd = path;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public 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.Change.Status.None, Models.Change.Status.Modified); break;
case " A": change.Set(Models.Change.Status.None, Models.Change.Status.Added); break;
case " D": change.Set(Models.Change.Status.None, Models.Change.Status.Deleted); break;
case " R": change.Set(Models.Change.Status.None, Models.Change.Status.Renamed); break;
case " C": change.Set(Models.Change.Status.None, Models.Change.Status.Copied); break;
case "M": change.Set(Models.Change.Status.Modified, Models.Change.Status.None); break;
case "MM": change.Set(Models.Change.Status.Modified, Models.Change.Status.Modified); break;
case "MD": change.Set(Models.Change.Status.Modified, Models.Change.Status.Deleted); break;
case "A": change.Set(Models.Change.Status.Added, Models.Change.Status.None); break;
case "AM": change.Set(Models.Change.Status.Added, Models.Change.Status.Modified); break;
case "AD": change.Set(Models.Change.Status.Added, Models.Change.Status.Deleted); break;
case "D": change.Set(Models.Change.Status.Deleted, Models.Change.Status.None); break;
case "R": change.Set(Models.Change.Status.Renamed, Models.Change.Status.None); break;
case "RM": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Modified); break;
case "RD": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Deleted); break;
case "C": change.Set(Models.Change.Status.Copied, Models.Change.Status.None); break;
case "CM": change.Set(Models.Change.Status.Copied, Models.Change.Status.Modified); break;
case "CD": change.Set(Models.Change.Status.Copied, Models.Change.Status.Deleted); break;
case "DR": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Renamed); break;
case "DC": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Copied); break;
case "DD": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Deleted); break;
case "AU": change.Set(Models.Change.Status.Added, Models.Change.Status.Unmerged); break;
case "UD": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Deleted); break;
case "UA": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Added); break;
case "DU": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Unmerged); break;
case "AA": change.Set(Models.Change.Status.Added, Models.Change.Status.Added); break;
case "UU": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Unmerged); break;
case "??": change.Set(Models.Change.Status.Untracked, Models.Change.Status.Untracked); break;
default: return;
}
changes.Add(change);
}
}
}

View file

@ -1,36 +1,21 @@
using System.Collections.Generic; using System;
using System.Text;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public class Merge : Command /// 合并分支
{ /// </summary>
public Merge(string repo, string source, string mode) public class Merge : Command {
{ private Action<string> handler = null;
WorkingDirectory = repo;
Context = repo; public Merge(string repo, string source, string mode, Action<string> onProgress) {
Cwd = repo;
Args = $"merge --progress {source} {mode}"; Args = $"merge --progress {source} {mode}";
TraitErrorAsOutput = true;
handler = onProgress;
} }
public Merge(string repo, List<string> targets, bool autoCommit, string strategy) public override void OnReadline(string line) {
{ handler?.Invoke(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();
} }
} }
} }

View file

@ -1,72 +0,0 @@
using System.IO;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class MergeTool
{
public static bool OpenForMerge(string repo, int toolType, string toolPath, string file)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.RaiseError = true;
// NOTE: If no <file> names are specified, 'git mergetool' will run the merge tool program on every file with merge conflicts.
var fileArg = string.IsNullOrEmpty(file) ? "" : $"\"{file}\"";
if (toolType == 0)
{
cmd.Args = $"mergetool {fileArg}";
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}";
return cmd.Exec();
}
public static bool OpenForDiff(string repo, int toolType, string toolPath, Models.DiffOption option)
{
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}";
return cmd.Exec();
}
}
}

View file

@ -1,18 +1,57 @@
namespace SourceGit.Commands using System;
{
public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase)
{
WorkingDirectory = repo;
Context = repo;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "pull --verbose --progress ";
if (useRebase) namespace SourceGit.Commands {
Args += "--rebase=true ";
/// <summary>
/// 拉回
/// </summary>
public class Pull : Command {
private Action<string> handler = null;
private bool needStash = false;
public Pull(string repo, string remote, string branch, bool useRebase, bool autoStash, Action<string> onProgress) {
Cwd = repo;
TraitErrorAsOutput = true;
handler = onProgress;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'");
Args = "";
} else {
Args = "-c credential.helper=manager-core ";
}
Args += "pull --verbose --progress --tags ";
if (useRebase) Args += "--rebase ";
if (autoStash) {
if (useRebase) Args += "--autostash ";
else needStash = true;
}
Args += $"{remote} {branch}"; Args += $"{remote} {branch}";
} }
public bool Run() {
if (needStash) {
var changes = new LocalChanges(Cwd).Result();
if (changes.Count > 0) {
if (!new Stash(Cwd).Push(changes, "PULL_AUTO_STASH")) {
return false;
}
} else {
needStash = false;
}
}
var succ = Exec();
if (needStash) new Stash(Cwd).Pop("stash@{0}");
return succ;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
}
} }
} }

View file

@ -1,37 +1,66 @@
namespace SourceGit.Commands using System;
{
public class Push : Command
{
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force)
{
WorkingDirectory = repo;
Context = repo;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "push --progress --verbose ";
if (withTags) namespace SourceGit.Commands {
Args += "--tags "; /// <summary>
if (checkSubmodules) /// 推送
Args += "--recurse-submodules=check "; /// </summary>
if (track) public class Push : Command {
Args += "-u "; private Action<string> handler = null;
if (force)
Args += "--force-with-lease "; public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress) {
Cwd = repo;
TraitErrorAsOutput = true;
handler = onProgress;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'");
Args = "";
} else {
Args = "-c credential.helper=manager-core ";
}
Args += "push --progress --verbose ";
if (withTags) Args += "--tags ";
if (track) Args += "-u ";
if (force) Args += "--force-with-lease ";
Args += $"{remote} {local}:{remoteBranch}"; Args += $"{remote} {local}:{remoteBranch}";
} }
public Push(string repo, string remote, string refname, bool isDelete) public Push(string repo, string remote, string branch) {
{ Cwd = repo;
WorkingDirectory = repo;
Context = repo;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "push ";
if (isDelete) var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args += "--delete "; if (!string.IsNullOrEmpty(sshKey)) {
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'");
Args = "";
} else {
Args = "-c credential.helper=manager-core ";
}
Args += $"{remote} {refname}"; Args += $"push {remote} --delete {branch}";
}
public Push(string repo, string remote, string tag, bool isDelete) {
Cwd = repo;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'");
Args = "";
} else {
Args = "-c credential.helper=manager-core ";
}
Args += "push ";
if (isDelete) Args += "--delete ";
Args += $"{remote} refs/tags/{tag}";
}
public override void OnReadline(string line) {
handler?.Invoke(line);
} }
} }
} }

View file

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

View file

@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public 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";
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)\"";
}
public List<Models.Branch> Result(out int localBranchesCount)
{
localBranchesCount = 0;
var branches = new List<Models.Branch>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return branches;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var remoteHeads = new Dictionary<string, string>();
foreach (var line in lines)
{
var b = ParseLine(line);
if (b != null)
{
branches.Add(b);
if (!b.IsLocal)
remoteHeads.Add(b.FullName, b.Head);
else
localBranchesCount++;
}
}
foreach (var b in branches)
{
if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream))
{
if (remoteHeads.TryGetValue(b.Upstream, out var upstreamHead))
{
b.IsUpstreamGone = false;
if (b.TrackStatus == null)
b.TrackStatus = new QueryTrackStatus(WorkingDirectory, b.Head, upstreamHead).Result();
}
else
{
b.IsUpstreamGone = true;
if (b.TrackStatus == null)
b.TrackStatus = new Models.BranchTrackStatus();
}
}
}
return branches;
}
private Models.Branch ParseLine(string line)
{
var parts = line.Split('\0');
if (parts.Length != 6)
return null;
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);
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal))
{
branch.Name = refName.Substring(PREFIX_LOCAL.Length);
branch.IsLocal = true;
}
else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal))
{
var name = refName.Substring(PREFIX_REMOTE.Length);
var shortNameIdx = name.IndexOf('/', StringComparison.Ordinal);
if (shortNameIdx < 0)
return null;
branch.Remote = name.Substring(0, shortNameIdx);
branch.Name = name.Substring(branch.Remote.Length + 1);
branch.IsLocal = false;
}
else
{
branch.Name = refName;
branch.IsLocal = true;
}
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;
}
}
}

View file

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

View file

@ -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;
}
}
}

View file

@ -1,34 +0,0 @@
namespace SourceGit.Commands
{
public class QueryCommitSignInfo : Command
{
public QueryCommitSignInfo(string repo, string sha, bool useFakeSignersFile)
{
WorkingDirectory = repo;
Context = repo;
const string baseArgs = "show --no-show-signature --format=%G?%n%GS%n%GK -s";
const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null";
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
}
public Models.CommitSignInfo Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
return null;
var raw = rs.StdOut.Trim().ReplaceLineEndings("\n");
if (raw.Length <= 1)
return null;
var lines = raw.Split('\n');
return new Models.CommitSignInfo()
{
VerifyResult = lines[0][0],
Signer = lines[1],
Key = lines[2]
};
}
}
}

View file

@ -1,154 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class QueryCommits : Command
{
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;
}
public List<Models.Commit> 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.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);
}
if (start < rs.StdOut.Length)
_current.Subject = rs.StdOut.Substring(start);
if (_findFirstMerged && !_isHeadFounded && _commits.Count > 0)
MarkFirstMerged();
return _commits;
}
private void ParseParent(string data)
{
if (data.Length < 8)
return;
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
private void MarkFirstMerged()
{
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0)
return;
var set = new HashSet<string>();
foreach (var sha in shas)
set.Add(sha);
foreach (var c in _commits)
{
if (set.Contains(c.SHA))
{
c.IsMerged = true;
break;
}
}
}
private List<Models.Commit> _commits = new List<Models.Commit>();
private Models.Commit _current = null;
private bool _findFirstMerged = false;
private bool _isHeadFounded = false;
}
}

View file

@ -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<Models.InteractiveCommit> 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<Models.InteractiveCommit> _commits = [];
private Models.InteractiveCommit _current = null;
private readonly string _boundary;
}
}

View file

@ -1,73 +1,26 @@
using System; using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace SourceGit.Commands namespace SourceGit.Commands {
{ /// <summary>
public static class QueryFileContent /// 取得指定提交下的某文件内容
{ /// </summary>
public static Stream Run(string repo, string revision, string file) public class QueryFileContent : Command {
{ private List<Models.TextLine> lines = new List<Models.TextLine>();
var starter = new ProcessStartInfo(); private int added = 0;
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitExecutable;
starter.Arguments = $"show {revision}:\"{file}\"";
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardOutput = true;
var stream = new MemoryStream(); public QueryFileContent(string repo, string commit, string path) {
try Cwd = repo;
{ Args = $"show {commit}:\"{path}\"";
var proc = new Process() { StartInfo = starter };
proc.Start();
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;
} }
public static Stream FromLFS(string repo, string oid, long size) public List<Models.TextLine> Result() {
{ Exec();
var starter = new ProcessStartInfo(); return lines;
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(); public override void OnReadline(string line) {
try added++;
{ lines.Add(new Models.TextLine() { Number = added, Data = line });
var proc = new Process() { StartInfo = starter };
proc.Start();
proc.StandardInput.WriteLine("version https://git-lfs.github.com/spec/v1");
proc.StandardInput.WriteLine($"oid sha256:{oid}");
proc.StandardInput.WriteLine($"size {size}");
proc.StandardOutput.BaseStream.CopyTo(stream);
proc.WaitForExit();
proc.Close();
stream.Position = 0;
}
catch (Exception e)
{
App.RaiseException(repo, $"Failed to query file content: {e}");
}
return stream;
} }
} }
} }

View file

@ -1,30 +0,0 @@
using System.Text.RegularExpressions;
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();
public QueryFileSize(string repo, string file, string revision)
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree {revision} -l -- \"{file}\"";
}
public long 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;
}
}
}

View file

@ -0,0 +1,50 @@
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 查询文件大小变化
/// </summary>
public class QueryFileSizeChange {
class QuerySizeCmd : Command {
public QuerySizeCmd(string repo, string path, string revision) {
Cwd = repo;
Args = $"cat-file -s {revision}:\"{path}\"";
}
public long Result() {
string data = ReadToEnd().Output;
long size;
if (!long.TryParse(data, out size)) size = 0;
return size;
}
}
private Models.FileSizeChange change = new Models.FileSizeChange();
public QueryFileSizeChange(string repo, string[] revisions, string path, string orgPath) {
if (revisions.Length == 0) {
change.NewSize = new FileInfo(Path.Combine(repo, path)).Length;
change.OldSize = new QuerySizeCmd(repo, path, "HEAD").Result();
} else if (revisions.Length == 1) {
change.NewSize = new QuerySizeCmd(repo, path, "HEAD").Result();
if (string.IsNullOrEmpty(orgPath)) {
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
} else {
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
}
} else {
change.NewSize = new QuerySizeCmd(repo, path, revisions[1]).Result();
if (string.IsNullOrEmpty(orgPath)) {
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
} else {
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
}
}
}
public Models.FileSizeChange Result() {
return change;
}
}
}

View file

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

View file

@ -1,26 +1,23 @@
using System.IO; using System.IO;
namespace SourceGit.Commands namespace SourceGit.Commands {
{
public class QueryGitDir : Command /// <summary>
{ /// 取得GitDir
public QueryGitDir(string workDir) /// </summary>
{ public class QueryGitDir : Command {
WorkingDirectory = workDir; public QueryGitDir(string workDir) {
Cwd = workDir;
Args = "rev-parse --git-dir"; Args = "rev-parse --git-dir";
RaiseError = false;
} }
public string Result() public string Result() {
{ var rs = ReadToEnd().Output;
var rs = ReadToEnd().StdOut; if (string.IsNullOrEmpty(rs)) return null;
if (string.IsNullOrEmpty(rs))
return null;
rs = rs.Trim(); rs = rs.Trim();
if (Path.IsPathRooted(rs)) if (Path.IsPathRooted(rs)) return rs;
return rs; return Path.GetFullPath(Path.Combine(Cwd, rs));
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
} }
} }
} }

View file

@ -0,0 +1,28 @@
using System;
namespace SourceGit.Commands {
/// <summary>
/// 取得一个LFS对象的信息
/// </summary>
public class QueryLFSObject : Command {
private Models.LFSObject obj = new Models.LFSObject();
public QueryLFSObject(string repo, string commit, string path) {
Cwd = repo;
Args = $"show {commit}:\"{path}\"";
}
public Models.LFSObject Result() {
Exec();
return obj;
}
public override void OnReadline(string line) {
if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) {
obj.OID = line.Substring(11).Trim();
} else if (line.StartsWith("size")) {
obj.Size = int.Parse(line.Substring(4).Trim());
}
}
}
}

View file

@ -0,0 +1,41 @@
namespace SourceGit.Commands {
/// <summary>
/// 查询LFS对象变更
/// </summary>
public class QueryLFSObjectChange : Command {
private Models.LFSChange change = new Models.LFSChange();
public QueryLFSObjectChange(string repo, string args) {
Cwd = repo;
Args = $"diff --ignore-cr-at-eol {args}";
}
public Models.LFSChange Result() {
Exec();
return change;
}
public override void OnReadline(string line) {
var ch = line[0];
if (ch == '-') {
if (change.Old == null) change.Old = new Models.LFSObject();
line = line.Substring(1);
if (line.StartsWith("oid sha256:")) {
change.Old.OID = line.Substring(11);
} else if (line.StartsWith("size ")) {
change.Old.Size = int.Parse(line.Substring(5));
}
} else if (ch == '+') {
if (change.New == null) change.New = new Models.LFSObject();
line = line.Substring(1);
if (line.StartsWith("oid sha256:")) {
change.New.OID = line.Substring(11);
} else if (line.StartsWith("size ")) {
change.New.Size = int.Parse(line.Substring(5));
}
} else if (line.StartsWith(" size ")) {
change.New.Size = change.Old.Size = int.Parse(line.Substring(6));
}
}
}
}

View file

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

View file

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

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