Compare commits

..

No commits in common. "master" and "v1.5" have entirely different histories.
master ... v1.5

758 changed files with 18508 additions and 72805 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/*

45
.gitignore vendored
View file

@ -1,41 +1,4 @@
.vs/
.vscode/
.idea/
*.sln.docstates
*.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
.idea
.vs
bin
obj

View file

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

BIN
Preview_Dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
Preview_Light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

207
README.md
View file

@ -1,207 +1,18 @@
# SourceGit - Opensource Git GUI client.
# SourceGit
[![stars](https://img.shields.io/github/stars/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/stargazers)
[![forks](https://img.shields.io/github/forks/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/forks)
[![license](https://img.shields.io/github/license/sourcegit-scm/sourcegit.svg)](LICENSE)
[![latest](https://img.shields.io/github/v/release/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/releases/latest)
[![downloads](https://img.shields.io/github/downloads/sourcegit-scm/sourcegit/total)](https://github.com/sourcegit-scm/sourcegit/releases)
开源的Git客户端仅用于Windows 10。单文件无需安装< 500KB
## Highlights
## 预览
* Supports Windows/macOS/Linux
* Opensource/Free
* Fast
* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)
* Built-in light/dark themes
* Customize theme
* Visual commit graph
* Supports SSH access with each remote
* GIT commands with GUI
* Clone/Fetch/Pull/Push...
* Merge/Rebase/Reset/Revert/Cherry-pick...
* Amend/Reword/Squash
* Interactive rebase
* Branches
* Remotes
* Tags
* Stashes
* Submodules
* Worktrees
* Archive
* Diff
* Save as patch/apply
* File histories
* Blame
* Revision Diffs
* Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend
* Git command logs
* Search commits
* GitFlow
* Git LFS
* Bisect
* Issue Link
* Workspace
* Custom Action
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
* DarkTheme
> [!WARNING]
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.
![Preview_Dark](./Preview_Dark.png)
## Translation Status
* LightTheme
You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md)
![Preview_Light](./Preview_Light.png)
## How to Use
**To use this tool, you need to install Git(>=2.25.1) first.**
## Thanks
You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
| OS | PATH |
|---------|-----------------------------------------------------|
| Windows | `%APPDATA%\SourceGit` |
| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` |
| macOS | `${HOME}/Library/Application Support/SourceGit` |
> [!TIP]
> * You can open this data storage directory from the main menu `Open Data Storage Directory`.
> * You can create a `data` folder next to the `SourceGit` executable to force this app to store data (user settings, downloaded avatars and crash logs) into it (Portable-Mode). Only works on Windows.
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
![Theme Light](./screenshots/theme_light.png)
* Custom
You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git). And welcome to share your own themes.
## Contributing
Everyone is welcome to submit a PR. Please make sure your PR is based on the latest `develop` branch and the target branch of PR is `develop`.
In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download):
```sh
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
dotnet restore
dotnet build
dotnet run --project src/SourceGit.csproj
```
Thanks to all the people who contribute.
[![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).
* [PUMA](https://gitee.com/whgfu) 配置默认User

View file

@ -1,89 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34714.143
# Visual Studio Version 16
VisualStudioVersion = 16.0.30011.22
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGit", "SourceGit\SourceGit.csproj", "{0A04DD59-7A6C-410C-B427-7DC8183993BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -91,32 +11,15 @@ Global
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
{0A04DD59-7A6C-410C-B427-7DC8183993BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A04DD59-7A6C-410C-B427-7DC8183993BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A04DD59-7A6C-410C-B427-7DC8183993BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A04DD59-7A6C-410C-B427-7DC8183993BD}.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}
SolutionGuid = {01F4EC04-5B3C-4D74-BB48-1C251B2D2853}
EndGlobalSection
EndGlobal

6
SourceGit/App.config Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false" />
</runtime>
</configuration>

BIN
SourceGit/App.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

14
SourceGit/App.manifest Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
</assembly>

15
SourceGit/App.xaml Normal file
View file

@ -0,0 +1,15 @@
<Application x:Class="SourceGit.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="OnAppStartup"
Deactivated="OnAppDeactivated">
<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/Dark.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

129
SourceGit/App.xaml.cs Normal file
View file

@ -0,0 +1,129 @@
using Microsoft.Win32;
using System;
using System.IO;
using System.Windows;
namespace SourceGit {
/// <summary>
/// Application.
/// </summary>
public partial class App : Application {
/// <summary>
/// Getter/Setter for Git preference.
/// </summary>
public static Git.Preference Preference {
get { return Git.Preference.Instance; }
set { Git.Preference.Instance = value; }
}
/// <summary>
/// Check if GIT has been configured.
/// </summary>
public static bool IsGitConfigured {
get {
return !string.IsNullOrEmpty(Preference.GitExecutable)
&& File.Exists(Preference.GitExecutable);
}
}
/// <summary>
/// Error handler.
/// </summary>
public static Action<string> OnError {
get;
set;
}
/// <summary>
/// Raise error message.
/// </summary>
/// <param name="message"></param>
public static void RaiseError(string message) {
OnError?.Invoke(message);
}
/// <summary>
/// Get popup manager by repository
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public static UI.PopupManager GetPopupManager(Git.Repository repo) {
var main = Current.MainWindow as UI.Launcher;
if (main == null) return null;
if (repo == null) return (main.Tabs[0].Page as UI.Manager).popupManager;
for (int i = 1; i < main.openedTabs.Items.Count; i++) {
var opened = main.openedTabs.Items[i] as UI.Launcher.Tab;
if (opened.Repo.Path == repo.Path) {
return (opened.Page as UI.Dashboard).popupManager;
}
}
return null;
}
/// <summary>
/// Startup event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppStartup(object sender, StartupEventArgs e) {
// Use this app as a sequence editor?
var args = e.Args;
if (args.Length > 1) {
if (args[0] == "--interactive-rebase") {
if (args.Length < 3) {
Environment.Exit(1);
return;
}
File.WriteAllText(args[2], File.ReadAllText(args[1]));
}
Environment.Exit(0);
return;
}
// Try auto configure git via registry.
if (!IsGitConfigured) {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var git = root.OpenSubKey("SOFTWARE\\GitForWindows");
if (git != null) {
Preference.GitExecutable = Path.Combine(
git.GetValue("InstallPath") as string,
"bin",
"git.exe");
}
}
// Apply themes
if (Preference.UIUseLightTheme) {
foreach (var rs in Current.Resources.MergedDictionaries) {
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/")) {
rs.Source = new Uri("pack://application:,,,/Resources/Themes/Light.xaml", UriKind.Absolute);
break;
}
}
}
// Show main window
Current.MainWindow = new UI.Launcher();
Current.MainWindow.Show();
}
/// <summary>
/// Deactivated event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppDeactivated(object sender, EventArgs e) {
Git.Preference.Save();
GC.Collect();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Same as BoolToVisibilityConverter.
/// </summary>
public class BoolToCollapsed : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Converters {
/// <summary>
/// Convert file status to brush
/// </summary>
public class FileStatusToColor : IValueConverter {
/// <summary>
/// Is only test local changes.
/// </summary>
public bool OnlyWorkTree { get; set; } = false;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var change = value as Git.Change;
if (change == null) return Brushes.Transparent;
var status = Git.Change.Status.None;
if (OnlyWorkTree) {
if (change.IsConflit) return Brushes.Yellow;
status = change.WorkTree;
} else {
status = change.Index;
}
if (App.Preference.UIUseLightTheme) {
switch (status) {
case Git.Change.Status.Modified: return Brushes.Goldenrod;
case Git.Change.Status.Added: return Brushes.Green;
case Git.Change.Status.Deleted: return Brushes.Red;
case Git.Change.Status.Renamed: return Brushes.Magenta;
case Git.Change.Status.Copied: return Brushes.Goldenrod;
case Git.Change.Status.Unmerged: return Brushes.Goldenrod;
case Git.Change.Status.Untracked: return Brushes.Green;
default: return Brushes.Transparent;
}
} else {
switch (status) {
case Git.Change.Status.Modified: return Brushes.DarkGoldenrod;
case Git.Change.Status.Added: return Brushes.DarkGreen;
case Git.Change.Status.Deleted: return Brushes.DarkRed;
case Git.Change.Status.Renamed: return Brushes.DarkMagenta;
case Git.Change.Status.Copied: return Brushes.DarkGoldenrod;
case Git.Change.Status.Unmerged: return Brushes.DarkGoldenrod;
case Git.Change.Status.Untracked: return Brushes.DarkGreen;
default: return Brushes.Transparent;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert file status to icon.
/// </summary>
public class FileStatusToIcon : IValueConverter {
/// <summary>
/// Is only test local changes.
/// </summary>
public bool OnlyWorkTree { get; set; } = false;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var change = value as Git.Change;
if (change == null) return "";
var status = Git.Change.Status.None;
if (OnlyWorkTree) {
if (change.IsConflit) return "X";
status = change.WorkTree;
} else {
status = change.Index;
}
switch (status) {
case Git.Change.Status.Modified: return "M";
case Git.Change.Status.Added: return "A";
case Git.Change.Status.Deleted: return "D";
case Git.Change.Status.Renamed: return "R";
case Git.Change.Status.Copied: return "C";
case Git.Change.Status.Unmerged: return "U";
case Git.Change.Status.Untracked: return "?";
default: return "?";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert indent(horizontal offset) to Margin property
/// </summary>
public class IndentToMargin : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return new Thickness((double)value, 0, 0, 0);
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return ((Thickness)value).Left;
}
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Inverse bool converter.
/// </summary>
public class InverseBool : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return !((bool)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Inverse BoolToCollapsed.
/// </summary>
public class InverseBoolToCollapsed : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Collapsed : Visibility.Visible;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert percent to double.
/// </summary>
public class PercentToDouble : IValueConverter {
/// <summary>
/// Percentage.
/// </summary>
public double Percent { get; set; }
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (double)value * Percent;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Converters {
/// <summary>
/// Convert depth of a TreeViewItem to Margin property.
/// </summary>
public class TreeViewItemDepthToMargin : IValueConverter {
/// <summary>
/// Indent length
/// </summary>
public double Indent { get; set; } = 19;
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
TreeViewItem item = value as TreeViewItem;
if (item == null) return new Thickness(0);
TreeViewItem iterator = GetParent(item);
int depth = 0;
while (iterator != null) {
depth++;
iterator = GetParent(iterator);
}
return new Thickness(Indent * depth, 0, 0, 0);
}
/// <summary>
/// Implement IValueConvert.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
/// <summary>
/// Get parent item.
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private TreeViewItem GetParent(TreeViewItem item) {
var parent = VisualTreeHelper.GetParent(item);
while (parent != null && !(parent is TreeView) && !(parent is TreeViewItem)) {
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
}
}

35
SourceGit/Git/Blame.cs Normal file
View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace SourceGit.Git {
/// <summary>
/// Blame
/// </summary>
public class Blame {
/// <summary>
/// Block content.
/// </summary>
public class Block {
public string CommitSHA { get; set; }
public string Author { get; set; }
public string Time { get; set; }
public string Content { get; set; }
}
/// <summary>
/// Blocks
/// </summary>
public List<Block> Blocks { get; set; } = new List<Block>();
/// <summary>
/// Is binary file?
/// </summary>
public bool IsBinary { get; set; } = false;
/// <summary>
/// Line count.
/// </summary>
public int LineCount { get; set; } = 0;
}
}

190
SourceGit/Git/Branch.cs Normal file
View file

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git branch
/// </summary>
public class Branch {
private static readonly string PRETTY_FORMAT = @"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)";
private static readonly Regex PARSE = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
private static readonly Regex AHEAD = new Regex(@"ahead (\d+)");
private static readonly Regex BEHIND = new Regex(@"behind (\d+)");
/// <summary>
/// Branch type.
/// </summary>
public enum Type {
Normal,
Feature,
Release,
Hotfix,
}
/// <summary>
/// Branch name
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Full name.
/// </summary>
public string FullName { get; set; } = "";
/// <summary>
/// Head ref
/// </summary>
public string Head { get; set; } = "";
/// <summary>
/// Subject for head ref.
/// </summary>
public string HeadSubject { get; set; } = "";
/// <summary>
/// Is local branch
/// </summary>
public bool IsLocal { get; set; } = false;
/// <summary>
/// Branch type.
/// </summary>
public Type Kind { get; set; } = Type.Normal;
/// <summary>
/// Remote name. Only used for remote branch
/// </summary>
public string Remote { get; set; } = "";
/// <summary>
/// Upstream. Only used for local branches.
/// </summary>
public string Upstream { get; set; }
/// <summary>
/// Track information for upstream. Only used for local branches.
/// </summary>
public string UpstreamTrack { get; set; }
/// <summary>
/// Is current branch. Only used for local branches.
/// </summary>
public bool IsCurrent { get; set; }
/// <summary>
/// Is this branch's HEAD same with upstream?
/// </summary>
public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load branches.
/// </summary>
/// <param name="repo"></param>
public static List<Branch> Load(Repository repo) {
var localPrefix = "refs/heads/";
var remotePrefix = "refs/remotes/";
var branches = new List<Branch>();
var remoteBranches = new List<string>();
repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => {
var match = PARSE.Match(line);
if (!match.Success) return;
var branch = new Branch();
var refname = match.Groups[1].Value;
if (refname.EndsWith("/HEAD")) return;
if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) {
branch.Name = refname.Substring(localPrefix.Length);
branch.IsLocal = true;
} else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) {
var name = refname.Substring(remotePrefix.Length);
branch.Remote = name.Substring(0, name.IndexOf('/'));
branch.Name = name;
branch.IsLocal = false;
remoteBranches.Add(refname);
}
branch.FullName = refname;
branch.Head = match.Groups[2].Value;
branch.IsCurrent = match.Groups[3].Value == "*";
branch.Upstream = match.Groups[4].Value;
branch.UpstreamTrack = ParseTrack(match.Groups[5].Value);
branch.HeadSubject = match.Groups[6].Value;
branches.Add(branch);
});
// Fixed deleted remote branch
foreach (var b in branches) {
if (!string.IsNullOrEmpty(b.Upstream) && !remoteBranches.Contains(b.Upstream)) {
b.Upstream = null;
}
}
return branches;
}
/// <summary>
/// Create new branch.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
public static void Create(Repository repo, string name, string startPoint) {
var errs = repo.RunCommand($"branch {name} {startPoint}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Rename branch
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
public void Rename(Repository repo, string name) {
var errs = repo.RunCommand($"branch -M {Name} {name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Delete branch.
/// </summary>
/// <param name="repo"></param>
public void Delete(Repository repo) {
string errs = null;
if (!IsLocal) {
errs = repo.RunCommand($"-c credential.helper=manager push {Remote} --delete {Name.Substring(Name.IndexOf('/')+1)}", null);
} else {
errs = repo.RunCommand($"branch -D {Name}", null);
}
if (errs != null) App.RaiseError(errs);
}
private static string ParseTrack(string data) {
if (string.IsNullOrEmpty(data)) return "";
string track = "";
var ahead = AHEAD.Match(data);
if (ahead.Success) {
track += ahead.Groups[1].Value + "↑ ";
}
var behind = BEHIND.Match(data);
if (behind.Success) {
track += behind.Groups[1].Value + "↓";
}
return track.Trim();
}
}
}

147
SourceGit/Git/Change.cs Normal file
View file

@ -0,0 +1,147 @@
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Changed file status.
/// </summary>
public class Change {
private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
/// <summary>
/// Status Code
/// </summary>
public enum Status {
None,
Modified,
Added,
Deleted,
Renamed,
Copied,
Unmerged,
Untracked,
}
/// <summary>
/// Index status
/// </summary>
public Status Index { get; set; }
/// <summary>
/// Work tree status.
/// </summary>
public Status WorkTree { get; set; }
/// <summary>
/// Current file path.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Original file path before this revision.
/// </summary>
public string OriginalPath { get; set; }
/// <summary>
/// Staged(added) in index?
/// </summary>
public bool IsAddedToIndex {
get {
if (Index == Status.None || Index == Status.Untracked) return false;
return true;
}
}
/// <summary>
/// Is conflict?
/// </summary>
public bool IsConflit {
get {
if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true;
if (Index == Status.Added && WorkTree == Status.Added) return true;
if (Index == Status.Deleted && WorkTree == Status.Deleted) return true;
return false;
}
}
/// <summary>
/// Parse change for `--name-status` data.
/// </summary>
/// <param name="data">Raw data.</param>
/// <param name="fromCommit">Read from commit?</param>
/// <returns>Parsed change instance.</returns>
public static Change Parse(string data, bool fromCommit = false) {
var match = FORMAT.Match(data);
if (!match.Success) return null;
var change = new Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
if (fromCommit) {
switch (status[0]) {
case 'M': change.Set(Status.Modified); break;
case 'A': change.Set(Status.Added); break;
case 'D': change.Set(Status.Deleted); break;
case 'R': change.Set(Status.Renamed); break;
case 'C': change.Set(Status.Copied); break;
default: return null;
}
} else {
switch (status) {
case " M": change.Set(Status.None, Status.Modified); break;
case " A": change.Set(Status.None, Status.Added); break;
case " D": change.Set(Status.None, Status.Deleted); break;
case " R": change.Set(Status.None, Status.Renamed); break;
case " C": change.Set(Status.None, Status.Copied); break;
case "M": change.Set(Status.Modified, Status.None); break;
case "MM": change.Set(Status.Modified, Status.Modified); break;
case "MD": change.Set(Status.Modified, Status.Deleted); break;
case "A": change.Set(Status.Added, Status.None); break;
case "AM": change.Set(Status.Added, Status.Modified); break;
case "AD": change.Set(Status.Added, Status.Deleted); break;
case "D": change.Set(Status.Deleted, Status.None); break;
case "R": change.Set(Status.Renamed, Status.None); break;
case "RM": change.Set(Status.Renamed, Status.Modified); break;
case "RD": change.Set(Status.Renamed, Status.Deleted); break;
case "C": change.Set(Status.Copied, Status.None); break;
case "CM": change.Set(Status.Copied, Status.Modified); break;
case "CD": change.Set(Status.Copied, Status.Deleted); break;
case "DR": change.Set(Status.Deleted, Status.Renamed); break;
case "DC": change.Set(Status.Deleted, Status.Copied); break;
case "DD": change.Set(Status.Deleted, Status.Deleted); break;
case "AU": change.Set(Status.Added, Status.Unmerged); break;
case "UD": change.Set(Status.Unmerged, Status.Deleted); break;
case "UA": change.Set(Status.Unmerged, Status.Added); break;
case "DU": change.Set(Status.Deleted, Status.Unmerged); break;
case "AA": change.Set(Status.Added, Status.Added); break;
case "UU": change.Set(Status.Unmerged, Status.Unmerged); break;
case "??": change.Set(Status.Untracked, Status.Untracked); break;
default: return null;
}
}
if (change.Path[0] == '"') change.Path = change.Path.Substring(1, change.Path.Length - 2);
if (!string.IsNullOrEmpty(change.OriginalPath) && change.OriginalPath[0] == '"') change.OriginalPath = change.OriginalPath.Substring(1, change.OriginalPath.Length - 2);
return change;
}
private void Set(Status index, Status workTree = Status.None) {
Index = index;
WorkTree = workTree;
if (index == Status.Renamed || workTree == Status.Renamed) {
var idx = Path.IndexOf('\t');
if (idx >= 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 1);
} else {
idx = Path.IndexOf(" -> ");
if (idx > 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 4);
}
}
}
}
}
}

296
SourceGit/Git/Commit.cs Normal file
View file

@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git commit information.
/// </summary>
public class Commit {
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
/// <summary>
/// Object in commit.
/// </summary>
public class Object {
public enum Type {
Tag,
Blob,
Tree,
Commit,
}
public string Path { get; set; }
public Type Kind { get; set; }
public string SHA { get; set; }
}
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Short SHA.
/// </summary>
public string ShortSHA => SHA.Substring(0, 8);
/// <summary>
/// Parent commit SHAs.
/// </summary>
public List<string> Parents { get; set; } = new List<string>();
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Committer.
/// </summary>
public User Committer { get; set; } = new User();
/// <summary>
/// Subject
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// Extra message.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// HEAD commit?
/// </summary>
public bool IsHEAD { get; set; } = false;
/// <summary>
/// Merged in current branch?
/// </summary>
public bool IsMerged { get; set; } = false;
/// <summary>
/// X offset in graph
/// </summary>
public double GraphOffset { get; set; } = 0;
/// <summary>
/// Has decorators.
/// </summary>
public bool HasDecorators => Decorators.Count > 0;
/// <summary>
/// Decorators.
/// </summary>
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
/// <summary>
/// Read commits.
/// </summary>
/// <param name="repo">Repository</param>
/// <param name="limit">Limitations</param>
/// <returns>Parsed commits.</returns>
public static List<Commit> Load(Repository repo, string limit) {
List<Commit> commits = new List<Commit>();
Commit current = null;
bool bSkippingGpgsig = false;
bool findHead = false;
repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => {
if (bSkippingGpgsig) {
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false;
return;
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) {
bSkippingGpgsig = true;
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
current = new Commit();
ParseSHA(current, line.Substring("commit ".Length));
if (!findHead) findHead = current.IsHEAD;
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");
}
});
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
if (!findHead && commits.Count > 0) {
var startInfo = new ProcessStartInfo();
startInfo.FileName = Preference.Instance.GitExecutable;
startInfo.Arguments = $"merge-base --is-ancestor {commits[0].SHA} HEAD";
startInfo.WorkingDirectory = repo.Path;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = false;
startInfo.RedirectStandardError = false;
var proc = new Process() { StartInfo = startInfo };
proc.Start();
proc.WaitForExit();
commits[0].IsMerged = proc.ExitCode == 0;
proc.Close();
}
return commits;
}
/// <summary>
/// Get changed file list.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
var changes = new List<Change>();
var regex = new Regex(@"^[MADRC]\d*\s*.*$");
var errs = repo.RunCommand($"show --name-status {SHA}", line => {
if (!regex.IsMatch(line)) return;
var change = Change.Parse(line, true);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Get revision files.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Object> GetFiles(Repository repo) {
var files = new List<Object>();
var test = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
var errs = repo.RunCommand($"ls-tree -r {SHA}", line => {
var match = test.Match(line);
if (!match.Success) return;
var obj = new Object();
obj.Path = match.Groups[3].Value;
obj.Kind = Object.Type.Blob;
obj.SHA = match.Groups[2].Value;
switch (match.Groups[1].Value) {
case "tag": obj.Kind = Object.Type.Tag; break;
case "blob": obj.Kind = Object.Type.Blob; break;
case "tree": obj.Kind = Object.Type.Tree; break;
case "commit": obj.Kind = Object.Type.Commit; break;
}
files.Add(obj);
});
if (errs != null) App.RaiseError(errs);
return files;
}
/// <summary>
/// Get file content.
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <returns></returns>
public string GetTextFileContent(Repository repo, string file, out bool isBinary) {
var data = new List<string>();
var count = 0;
var binary = false;
var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => {
if (binary) return;
count++;
if (data.Count >= 1000) return;
if (line.IndexOf('\0') >= 0) {
binary = true;
data.Clear();
data.Add("BINARY FILE PREVIEW NOT SUPPORTED!");
return;
}
data.Add(line);
});
if (!binary && count > 1000) {
data.Add("...");
data.Add($"Total {count} lines. Hide {count-1000} lines.");
}
isBinary = binary;
if (errs != null) App.RaiseError(errs);
return string.Join("\n", data);
}
private static void ParseSHA(Commit commit, string data) {
var decoratorStart = data.IndexOf('(');
if (decoratorStart < 0) {
commit.SHA = data.Trim();
return;
}
commit.SHA = data.Substring(0, decoratorStart).Trim();
var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs) {
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.Tag,
Name = d.Substring(15).Trim()
});
} else if (d.EndsWith("/HEAD")) {
continue;
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) {
commit.IsHEAD = true;
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim()
});
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim()
});
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim()
});
}
}
}
}
}

View file

@ -0,0 +1,21 @@
namespace SourceGit.Git {
/// <summary>
/// Decorator type.
/// </summary>
public enum DecoratorType {
None,
CurrentBranchHead,
LocalBranchHead,
RemoteBranchHead,
Tag,
}
/// <summary>
/// Commit decorator.
/// </summary>
public class Decorator {
public DecoratorType Type { get; set; }
public string Name { get; set; }
}
}

263
SourceGit/Git/Diff.cs Normal file
View file

@ -0,0 +1,263 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Diff helper.
/// </summary>
public class Diff {
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
/// <summary>
/// Line mode.
/// </summary>
public enum LineMode {
Normal,
Indicator,
Empty,
Added,
Deleted,
}
/// <summary>
/// Side
/// </summary>
public enum Side {
Left,
Right,
Both,
}
/// <summary>
/// Binary change.
/// </summary>
public class BinaryChange {
public long Size = 0;
public long PreSize = 0;
}
/// <summary>
/// Block
/// </summary>
public class Block {
public Side Side = Side.Both;
public LineMode Mode = LineMode.Normal;
public int LeftStart = 0;
public int RightStart = 0;
public int Count = 0;
public StringBuilder Builder = new StringBuilder();
public bool IsLeftDelete => Side == Side.Left && Mode == LineMode.Deleted;
public bool IsRightAdded => Side == Side.Right && Mode == LineMode.Added;
public bool IsBothSideNormal => Side == Side.Both && Mode == LineMode.Normal;
public bool CanShowNumber => Mode != LineMode.Indicator && Mode != LineMode.Empty;
public void Append(string data) {
if (Count > 0) Builder.AppendLine();
Builder.Append(data);
Count++;
}
}
/// <summary>
/// Diff result.
/// </summary>
public class Result {
public bool IsValid = false;
public bool IsBinary = false;
public List<Block> Blocks = new List<Block>();
public int LeftLineCount = 0;
public int RightLineCount = 0;
public void SetBinary() {
IsValid = true;
IsBinary = true;
}
public void Add(Block b) {
if (b.Count == 0) return;
switch (b.Side) {
case Side.Left:
LeftLineCount += b.Count;
break;
case Side.Right:
RightLineCount += b.Count;
break;
default:
LeftLineCount += b.Count;
RightLineCount += b.Count;
break;
}
Blocks.Add(b);
}
public void Fit() {
if (LeftLineCount > RightLineCount) {
var b = new Block();
b.Side = Side.Right;
b.Mode = LineMode.Empty;
var delta = LeftLineCount - RightLineCount;
for (int i = 0; i < delta; i++) b.Append("");
Add(b);
} else if (LeftLineCount < RightLineCount) {
var b = new Block();
b.Side = Side.Left;
b.Mode = LineMode.Empty;
var delta = RightLineCount - LeftLineCount;
for (int i = 0; i < delta; i++) b.Append("");
Add(b);
}
}
}
/// <summary>
/// Run diff process.
/// </summary>
/// <param name="repo"></param>
/// <param name="args"></param>
/// <returns></returns>
public static Result Run(Repository repo, string args) {
var rs = new Result();
var current = new Block();
var left = 0;
var right = 0;
repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => {
if (rs.IsBinary) return;
if (!rs.IsValid) {
var match = REG_INDICATOR.Match(line);
if (!match.Success) {
if (line.StartsWith("Binary ")) rs.SetBinary();
return;
}
rs.IsValid = true;
left = int.Parse(match.Groups[1].Value);
right = int.Parse(match.Groups[2].Value);
current.Mode = LineMode.Indicator;
current.Append(line);
} else {
if (line[0] == '-') {
if (current.IsLeftDelete) {
current.Append(line.Substring(1));
} else {
rs.Add(current);
current = new Block();
current.Side = Side.Left;
current.Mode = LineMode.Deleted;
current.LeftStart = left;
current.Append(line.Substring(1));
}
left++;
} else if (line[0] == '+') {
if (current.IsRightAdded) {
current.Append(line.Substring(1));
} else {
rs.Add(current);
current = new Block();
current.Side = Side.Right;
current.Mode = LineMode.Added;
current.RightStart = right;
current.Append(line.Substring(1));
}
right++;
} else if (line[0] == '\\') {
var tmp = new Block();
tmp.Side = current.Side;
tmp.Mode = LineMode.Indicator;
tmp.Append(line.Substring(1));
rs.Add(current);
rs.Add(tmp);
rs.Fit();
current = new Block();
current.LeftStart = left;
current.RightStart = right;
} else {
var match = REG_INDICATOR.Match(line);
if (match.Success) {
rs.Add(current);
rs.Fit();
left = int.Parse(match.Groups[1].Value);
right = int.Parse(match.Groups[2].Value);
current = new Block();
current.Mode = LineMode.Indicator;
current.Append(line);
} else {
if (current.IsBothSideNormal) {
current.Append(line.Substring(1));
} else {
rs.Add(current);
rs.Fit();
current = new Block();
current.LeftStart = left;
current.RightStart = right;
current.Append(line.Substring(1));
}
left++;
right++;
}
}
}
});
rs.Add(current);
rs.Fit();
if (rs.IsBinary) rs.Blocks.Clear();
return rs;
}
/// <summary>
/// Get file size changes for binary file.
/// </summary>
/// <param name="repo"></param>
/// <param name="revisions"></param>
/// <param name="path"></param>
/// <param name="orgPath"></param>
/// <returns></returns>
public static BinaryChange GetSizeChange(Repository repo, string[] revisions, string path, string orgPath = null) {
var change = new BinaryChange();
if (revisions.Length == 0) { // Compare working copy with HEAD
change.Size = new FileInfo(Path.Combine(repo.Path, path)).Length;
change.PreSize = repo.GetFileSize("HEAD", path);
} else if (revisions.Length == 1) { // Compare HEAD with given revision.
change.Size = repo.GetFileSize("HEAD", path);
if (!string.IsNullOrEmpty(orgPath)) {
change.PreSize = repo.GetFileSize(revisions[0], orgPath);
} else {
change.PreSize = repo.GetFileSize(revisions[0], path);
}
} else {
change.Size = repo.GetFileSize(revisions[1], path);
if (!string.IsNullOrEmpty(orgPath)) {
change.PreSize = repo.GetFileSize(revisions[0], orgPath);
} else {
change.PreSize = repo.GetFileSize(revisions[0], path);
}
}
return change;
}
}
}

202
SourceGit/Git/MergeTool.cs Normal file
View file

@ -0,0 +1,202 @@
using Microsoft.Win32;
using SourceGit.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// External merge tool
/// </summary>
public class MergeTool {
/// <summary>
/// Display name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Executable file name.
/// </summary>
public string ExecutableName { get; set; }
/// <summary>
/// Command line parameter.
/// </summary>
public string Parameter { get; set; }
/// <summary>
/// Auto finder.
/// </summary>
public Func<string> Finder { get; set; }
/// <summary>
/// Is this merge tool configured.
/// </summary>
public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName);
/// <summary>
/// Supported merge tools.
/// </summary>
public static List<MergeTool> Supported = new List<MergeTool>() {
new MergeTool("--", "", "", FindInvalid),
new MergeTool("Araxis Merge", "Compare.exe", "/wait /merge /3 /a1 \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindAraxisMerge),
new MergeTool("Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare),
new MergeTool("KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3),
new MergeTool("P4Merge", "p4merge.exe", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindP4Merge),
new MergeTool("Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge),
new MergeTool("Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge),
new MergeTool("Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode),
};
/// <summary>
/// Finder for invalid merge tool.
/// </summary>
/// <returns></returns>
public static string FindInvalid() {
return "--";
}
/// <summary>
/// Find araxis merge tool install path.
/// </summary>
/// <returns></returns>
public static string FindAraxisMerge() {
var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find kdiff3.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindKDiff3() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext");
if (kdiff == null) return "";
return kdiff.GetValue("diffcommand") as string;
}
/// <summary>
/// Finder for p4merge
/// </summary>
/// <returns></returns>
public static string FindP4Merge() {
var path = @"C:\Program Files\Perforce\p4merge.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find BComp.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindBCompare() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare");
if (bc == null) return "";
var exec = bc.GetValue("ExePath") as string;
var dir = Path.GetDirectoryName(exec);
return $"{dir}\\BComp.exe";
}
/// <summary>
/// Find TortoiseMerge.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindTortoiseMerge() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN");
if (tortoiseSVN == null) return "";
return tortoiseSVN.GetValue("TMergePath") as string;
}
/// <summary>
/// Find vsDiffMerge.exe.
/// </summary>
/// <returns></returns>
public static string FindVSMerge() {
var dir = @"C:\Program Files (x86)\Microsoft Visual Studio";
if (Directory.Exists($"{dir}\\2019")) {
dir += "\\2019";
} else if (Directory.Exists($"{dir}\\2017")) {
dir += "\\2017";
} else {
return "";
}
if (Directory.Exists($"{dir}\\Community")) {
dir += "\\Community";
} else if (Directory.Exists($"{dir}\\Enterprise")) {
dir += "\\Enterprise";
} else if (Directory.Exists($"{dir}\\Professional")) {
dir += "\\Professional";
} else {
return "";
}
return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe";
}
/// <summary>
/// Find VSCode executable file path.
/// </summary>
/// <returns></returns>
public static string FindVSCode() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
return "";
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name"></param>
/// <param name="exe"></param>
/// <param name="param"></param>
/// <param name="finder"></param>
public MergeTool(string name, string exe, string param, Func<string> finder) {
Name = name;
ExecutableName = exe;
Parameter = param;
Finder = finder;
}
}
}

300
SourceGit/Git/Preference.cs Normal file
View file

@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace SourceGit.Git {
/// <summary>
/// User's preference settings. Serialized to
/// </summary>
public class Preference {
/// <summary>
/// Group(Virtual folder) for watched repositories.
/// </summary>
public class Group {
/// <summary>
/// Unique ID of this group.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Parent ID.
/// </summary>
public string ParentId { get; set; }
/// <summary>
/// Cache UI IsExpended status.
/// </summary>
public bool IsExpended { get; set; }
}
#region STATICS
/// <summary>
/// Storage path for Preference.
/// </summary>
private static readonly string SAVE_PATH = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"preference.xml");
/// <summary>
/// Runtime singleton instance.
/// </summary>
private static Preference instance = null;
public static Preference Instance {
get {
if (instance == null) Load();
return instance;
}
set {
instance = value;
}
}
#endregion
#region SETTING_GIT
/// <summary>
/// Git executable file path.
/// </summary>
public string GitExecutable { get; set; }
/// <summary>
/// Default clone directory.
/// </summary>
public string GitDefaultCloneDir { get; set; }
#endregion
#region SETTING_MERGE_TOOL
/// <summary>
/// Selected merge tool.
/// </summary>
public int MergeTool { get; set; } = 0;
/// <summary>
/// Executable file path for merge tool.
/// </summary>
public string MergeExecutable { get; set; } = "--";
#endregion
#region SETTING_UI
/// <summary>
/// Main window's width
/// </summary>
public double UIMainWindowWidth { get; set; }
/// <summary>
/// Main window's height
/// </summary>
public double UIMainWindowHeight { get; set; }
/// <summary>
/// Use light color theme.
/// </summary>
public bool UIUseLightTheme { get; set; }
/// <summary>
/// Show/Hide tags' list view.
/// </summary>
public bool UIShowTags { get; set; } = true;
/// <summary>
/// Use horizontal layout for histories.
/// </summary>
public bool UIUseHorizontalLayout { get; set; }
/// <summary>
/// Use list instead of tree in unstaged view
/// </summary>
public bool UIUseListInUnstaged { get; set; }
/// <summary>
/// Use list instead of tree in staged view.
/// </summary>
public bool UIUseListInStaged { get; set; }
/// <summary>
/// Use list instead of tree in change view.
/// </summary>
public bool UIUseListInChanges { get; set; }
#endregion
#region SETTING_REPOS
/// <summary>
/// Groups for repositories.
/// </summary>
public List<Group> Groups { get; set; } = new List<Group>();
/// <summary>
/// Watched repositories.
/// </summary>
public List<Repository> Repositories { get; set; } = new List<Git.Repository>();
#endregion
#region METHODS_LOAD_SAVE
/// <summary>
/// Load preference from disk.
/// </summary>
/// <returns>Loaded preference instance.</returns>
public static void Load() {
if (!File.Exists(SAVE_PATH)) {
instance = new Preference();
return;
}
try {
var stream = new FileStream(SAVE_PATH, FileMode.Open);
var reader = new XmlSerializer(typeof(Preference));
instance = (Preference)reader.Deserialize(stream);
stream.Close();
} catch {
instance = new Preference();
}
}
/// <summary>
/// Save current preference into disk.
/// </summary>
public static void Save() {
if (instance == null) return;
var dir = Path.GetDirectoryName(SAVE_PATH);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var stream = new FileStream(SAVE_PATH, FileMode.Create);
var writer = new XmlSerializer(typeof(Preference));
writer.Serialize(stream, instance);
stream.Flush();
stream.Close();
}
#endregion
#region METHODS_ON_GROUP
/// <summary>
/// Add new group(virtual folder).
/// </summary>
/// <param name="name">Display name.</param>
/// <param name="parentId">Parent group ID.</param>
/// <returns>Added group instance.</returns>
public Group AddGroup(string name, string parentId) {
var group = new Group() {
Name = name,
Id = Guid.NewGuid().ToString(),
ParentId = parentId,
IsExpended = false,
};
Groups.Add(group);
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
return group;
}
/// <summary>
/// Find group by ID.
/// </summary>
/// <param name="id">Unique ID</param>
/// <returns>Founded group's instance.</returns>
public Group FindGroup(string id) {
foreach (var group in Groups) {
if (group.Id == id) return group;
}
return null;
}
/// <summary>
/// Rename group.
/// </summary>
/// <param name="id">Unique ID</param>
/// <param name="newName">New name.</param>
public void RenameGroup(string id, string newName) {
foreach (var group in Groups) {
if (group.Id == id) {
group.Name = newName;
break;
}
}
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a group.
/// </summary>
/// <param name="id">Unique ID</param>
public void RemoveGroup(string id) {
int removedIdx = -1;
for (int i = 0; i < Groups.Count; i++) {
if (Groups[i].Id == id) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Groups.RemoveAt(removedIdx);
}
#endregion
#region METHODS_ON_REPOS
/// <summary>
/// Add repository.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="groupId">Group's ID</param>
/// <returns>Added repository instance.</returns>
public Repository AddRepository(string path, string groupId) {
var repo = FindRepository(path);
if (repo != null) return repo;
var dir = new DirectoryInfo(path);
repo = new Repository() {
Path = dir.FullName,
Name = dir.Name,
GroupId = groupId,
LastOpenTime = 0,
};
Repositories.Add(repo);
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
return repo;
}
/// <summary>
/// Find repository by path.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <returns>Founded repository instance.</returns>
public Repository FindRepository(string path) {
var dir = new DirectoryInfo(path);
foreach (var repo in Repositories) {
if (repo.Path == dir.FullName) return repo;
}
return null;
}
/// <summary>
/// Change a repository's display name in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="newName">New name</param>
public void RenameRepository(string path, string newName) {
var repo = FindRepository(path);
if (repo == null) return;
repo.Name = newName;
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a repository in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
public void RemoveRepository(string path) {
var dir = new DirectoryInfo(path);
var removedIdx = -1;
for (int i = 0; i < Repositories.Count; i++) {
if (Repositories[i].Path == dir.FullName) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Repositories.RemoveAt(removedIdx);
}
#endregion
}
}

97
SourceGit/Git/Remote.cs Normal file
View file

@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git remote
/// </summary>
public class Remote {
private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
/// <summary>
/// Name of this remote
/// </summary>
public string Name { get; set; }
/// <summary>
/// URL
/// </summary>
public string URL { get; set; }
/// <summary>
/// Parsing remote
/// </summary>
/// <param name="repo">Repository</param>
/// <returns></returns>
public static List<Remote> Load(Repository repo) {
var remotes = new List<Remote>();
var added = new List<string>();
repo.RunCommand("remote -v", data => {
var match = FORMAT.Match(data);
if (!match.Success) return;
var remote = new Remote() {
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (added.Contains(remote.Name)) return;
added.Add(remote.Name);
remotes.Add(remote);
});
return remotes;
}
/// <summary>
/// Add new remote
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
public static void Add(Repository repo, string name, string url) {
var errs = repo.RunCommand($"remote add {name} {url}", null);
if (errs != null) {
App.RaiseError(errs);
} else {
repo.Fetch(new Remote() { Name = name }, true, null);
}
}
/// <summary>
/// Delete remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="remote"></param>
public static void Delete(Repository repo, string remote) {
var errs = repo.RunCommand($"remote remove {remote}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Edit remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
public void Edit(Repository repo, string name, string url) {
string errs = null;
if (name != Name) {
errs = repo.RunCommand($"remote rename {Name} {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
}
if (url != URL) {
errs = repo.RunCommand($"remote set-url {name} {url}", null);
if (errs != null) App.RaiseError(errs);
}
}
}
}

1163
SourceGit/Git/Repository.cs Normal file

File diff suppressed because it is too large Load diff

101
SourceGit/Git/Stash.cs Normal file
View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// Git stash
/// </summary>
public class Stash {
/// <summary>
/// SHA for this stash
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Stash push.
/// </summary>
/// <param name="repo"></param>
/// <param name="includeUntracked"></param>
/// <param name="message"></param>
/// <param name="files"></param>
public static void Push(Repository repo, bool includeUntracked, string message, List<string> files) {
string specialFiles = "";
if (files.Count > 0) {
specialFiles = " --";
foreach (var f in files) specialFiles += $" \"{f}\"";
}
string args = "stash push ";
if (includeUntracked) args += "-u ";
if (!string.IsNullOrEmpty(message)) args += $"-m \"{message}\" ";
var errs = repo.RunCommand(args + specialFiles, null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Get changed file list in this stash.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
List<Change> changes = new List<Change>();
var errs = repo.RunCommand($"diff --name-status --pretty=format: {SHA}^ {SHA}", line => {
var change = Change.Parse(line);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Apply stash.
/// </summary>
/// <param name="repo"></param>
public void Apply(Repository repo) {
var errs = repo.RunCommand($"stash apply -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Pop stash
/// </summary>
/// <param name="repo"></param>
public void Pop(Repository repo) {
var errs = repo.RunCommand($"stash pop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Drop stash
/// </summary>
/// <param name="repo"></param>
public void Drop(Repository repo) {
var errs = repo.RunCommand($"stash drop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

118
SourceGit/Git/Tag.cs Normal file
View file

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git tag.
/// </summary>
public class Tag {
private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)");
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load all tags
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public static List<Tag> Load(Repository repo) {
var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
var tags = new List<Tag>();
repo.RunCommand(args, line => {
var match = FORMAT.Match(line);
if (!match.Success) return;
var name = match.Groups[1].Value;
var commit = match.Groups[2].Value;
var dereference = match.Groups[3].Value;
if (string.IsNullOrEmpty(dereference)) {
tags.Add(new Tag() {
Name = name,
SHA = commit,
});
} else {
tags.Add(new Tag() {
Name = name,
SHA = dereference,
});
}
});
return tags;
}
/// <summary>
/// Add new tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
/// <param name="message"></param>
public static void Add(Repository repo, string name, string startPoint, string message) {
var args = $"tag -a {name} {startPoint} ";
if (!string.IsNullOrEmpty(message)) {
string temp = Path.GetTempFileName();
File.WriteAllText(temp, message);
args += $"-F \"{temp}\"";
} else {
args += $"-m {name}";
}
var errs = repo.RunCommand(args, null);
if (errs != null) App.RaiseError(errs);
else repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Delete tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="push"></param>
public static void Delete(Repository repo, string name, bool push) {
var errs = repo.RunCommand($"tag --delete {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
if (push) {
var remotes = repo.Remotes();
foreach (var r in remotes) {
repo.RunCommand($"-c credential.helper=manager push --delete {r.Name} refs/tags/{name}", null);
}
}
repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Push tag to remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="remote"></param>
public static void Push(Repository repo, string name, string remote) {
var errs = repo.RunCommand($"-c credential.helper=manager push {remote} refs/tags/{name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

42
SourceGit/Git/User.cs Normal file
View file

@ -0,0 +1,42 @@
using System;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git user.
/// </summary>
public class User {
private static readonly Regex FORMAT = new Regex(@"\w+ (.*) <([\w\.\-_]+@[\w\.\-_]+)> (\d{10}) [\+\-]\d+");
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Email.
/// </summary>
public string Email { get; set; } = "";
/// <summary>
/// Operation time.
/// </summary>
public string Time { get; set; } = "";
/// <summary>
/// Parse user from raw string.
/// </summary>
/// <param name="data">Raw string</param>
public void Parse(string data) {
var match = FORMAT.Match(data);
if (!match.Success) return;
var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(int.Parse(match.Groups[3].Value));
Name = match.Groups[1].Value;
Email = match.Groups[2].Value;
Time = time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
}
}

View file

@ -0,0 +1,275 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Tools to parse commit graph.
/// </summary>
public class CommitGraphMaker {
/// <summary>
/// Sizes
/// </summary>
public static readonly double UNIT_WIDTH = 12;
public static readonly double HALF_WIDTH = 6;
public static readonly double DOUBLE_WIDTH = 24;
public static readonly double UNIT_HEIGHT = 24;
public static readonly double HALF_HEIGHT = 12;
/// <summary>
/// Colors
/// </summary>
public static Brush[] Colors = new Brush[] {
Brushes.Orange,
Brushes.ForestGreen,
Brushes.Gold,
Brushes.Magenta,
Brushes.Red,
Brushes.Gray,
Brushes.Turquoise,
Brushes.Olive,
};
/// <summary>
/// Helpers to draw lines.
/// </summary>
public class LineHelper {
private double lastX = 0;
private double lastY = 0;
/// <summary>
/// Parent commit id.
/// </summary>
public string Next { get; set; }
/// <summary>
/// Is merged into this tree.
/// </summary>
public bool IsMerged { get; set; }
/// <summary>
/// Points in line
/// </summary>
public List<Point> Points { get; set; }
/// <summary>
/// Brush to draw line
/// </summary>
public Brush Brush { get; set; }
/// <summary>
/// Current horizontal offset.
/// </summary>
public double HorizontalOffset => lastX;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="nextCommitId">Parent commit id</param>
/// <param name="isMerged">Is merged in tree</param>
/// <param name="colorIdx">Color index</param>
/// <param name="startPoint">Start point</param>
public LineHelper(string nextCommitId, bool isMerged, int colorIdx, Point startPoint) {
Next = nextCommitId;
IsMerged = isMerged;
Points = new List<Point>() { startPoint };
Brush = Colors[colorIdx % Colors.Length];
lastX = startPoint.X;
lastY = startPoint.Y;
}
/// <summary>
/// Line to.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="isEnd"></param>
public void AddPoint(double x, double y, bool isEnd = false) {
if (x > lastX) {
Points.Add(new Point(lastX, lastY));
Points.Add(new Point(x, y - HALF_HEIGHT));
} else if (x < lastX) {
Points.Add(new Point(lastX, lastY + HALF_HEIGHT));
Points.Add(new Point(x, y));
}
lastX = x;
lastY = y;
if (isEnd) {
var last = Points.Last();
if (last.X != lastX || last.Y != lastY) Points.Add(new Point(lastX, lastY));
}
}
}
/// <summary>
/// Short link between two commits.
/// </summary>
public struct ShortLink {
public Point Start;
public Point Control;
public Point End;
public Brush Brush;
}
/// <summary>
/// Dot
/// </summary>
public struct Dot {
public double X;
public double Y;
public Brush Color;
}
/// <summary>
/// Independent lines in graph
/// </summary>
public List<LineHelper> Lines { get; set; } = new List<LineHelper>();
/// <summary>
/// Short links.
/// </summary>
public List<ShortLink> Links { get; set; } = new List<ShortLink>();
/// <summary>
/// All dots.
/// </summary>
public List<Dot> Dots { get; set; } = new List<Dot>();
/// <summary>
/// Highlight commit id.
/// </summary>
public string Highlight { get; set; }
/// <summary>
/// Parse commits.
/// </summary>
/// <param name="commits"></param>
/// <returns></returns>
public static CommitGraphMaker Parse(List<Git.Commit> commits) {
CommitGraphMaker maker = new CommitGraphMaker();
List<LineHelper> unsolved = new List<LineHelper>();
List<LineHelper> ended = new List<LineHelper>();
Dictionary<string, LineHelper> currentMap = new Dictionary<string, LineHelper>();
double offsetY = -HALF_HEIGHT;
int colorIdx = 0;
for (int i = 0; i < commits.Count; i++) {
Git.Commit commit = commits[i];
LineHelper major = null;
bool isMerged = commit.IsHEAD || commit.IsMerged;
int oldCount = unsolved.Count;
// 更新Y坐标
offsetY += UNIT_HEIGHT;
// 找到当前的分支的HEAD用于默认选中
if (maker.Highlight == null && commit.IsHEAD) {
maker.Highlight = commit.SHA;
}
// 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合)
double offsetX = -HALF_WIDTH;
foreach (var l in unsolved) {
if (l.Next == commit.SHA) {
if (major == null) {
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0) {
major.Next = commit.Parents[0];
if (!currentMap.ContainsKey(major.Next)) currentMap.Add(major.Next, major);
} else {
major.Next = "ENDED";
ended.Add(l);
}
major.AddPoint(offsetX, offsetY);
} else {
ended.Add(l);
}
isMerged = isMerged || l.IsMerged;
} else {
if (!currentMap.ContainsKey(l.Next)) currentMap.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.AddPoint(offsetX, offsetY);
}
}
// 处理本提交为非当前分支HEAD的情况创建新依赖线路
if (major == null && commit.Parents.Count > 0) {
offsetX += UNIT_WIDTH;
major = new LineHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major);
colorIdx++;
}
// 确定本提交的点的位置
Point position = new Point(offsetX, offsetY);
if (major != null) {
major.IsMerged = isMerged;
position.X = major.HorizontalOffset;
position.Y = offsetY;
maker.Dots.Add(new Dot() { X = position.X - 3, Y = position.Y - 3, Color = major.Brush });
} else {
maker.Dots.Add(new Dot() { X = position.X - 3, Y = position.Y - 3, Color = Brushes.Orange });
}
// 处理本提交的其他依赖
for (int j = 1; j < commit.Parents.Count; j++) {
var parent = commit.Parents[j];
if (currentMap.ContainsKey(parent)) {
var l = currentMap[parent];
var link = new ShortLink();
link.Start = position;
link.End = new Point(l.HorizontalOffset, offsetY + HALF_HEIGHT);
link.Control = new Point(link.End.X, link.Start.Y);
link.Brush = l.Brush;
maker.Links.Add(link);
} else {
offsetX += UNIT_WIDTH;
unsolved.Add(new LineHelper(commit.Parents[j], isMerged, colorIdx, position));
colorIdx++;
}
}
// 处理已终止的线
foreach (var l in ended) {
l.AddPoint(position.X, position.Y, true);
maker.Lines.Add(l);
unsolved.Remove(l);
}
// 加入本次提交
commit.IsMerged = isMerged;
commit.GraphOffset = System.Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH);
// 清理临时数据
ended.Clear();
currentMap.Clear();
}
// 处理尚未终结的线
for (int i = 0; i < unsolved.Count; i++) {
var path = unsolved[i];
path.AddPoint((i + 0.5) * UNIT_WIDTH, (commits.Count - 0.5) * UNIT_HEIGHT, true);
maker.Lines.Add(path);
}
unsolved.Clear();
// 处理默认选中异常
if (maker.Highlight == null && commits.Count > 0) {
maker.Highlight = commits[0].SHA;
}
return maker;
}
}
}

View file

@ -0,0 +1,224 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Attached properties to TextBox.
/// </summary>
public static class TextBoxHelper {
/// <summary>
/// Auto scroll on text changed or selection changed.
/// </summary>
public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.RegisterAttached(
"AutoScroll",
typeof(bool),
typeof(TextBoxHelper),
new PropertyMetadata(false, OnAutoScrollChanged));
/// <summary>
/// Placeholder property
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
"Placeholder",
typeof(string),
typeof(TextBoxHelper),
new PropertyMetadata(string.Empty, OnPlaceholderChanged));
/// <summary>
/// Vertical alignment for placeholder.
/// </summary>
public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.RegisterAttached(
"PlaceholderBaseline",
typeof(AlignmentY),
typeof(TextBoxHelper),
new PropertyMetadata(AlignmentY.Center));
/// <summary>
/// Property to store generated placeholder brush.
/// </summary>
public static readonly DependencyProperty PlaceholderBrushProperty = DependencyProperty.RegisterAttached(
"PlaceholderBrush",
typeof(Brush),
typeof(TextBoxHelper),
new PropertyMetadata(Brushes.Transparent));
/// <summary>
/// Setter for AutoScrollProperty
/// </summary>
/// <param name="element"></param>
/// <param name="enabled"></param>
public static void SetAutoScroll(UIElement element, bool enabled) {
element.SetValue(AutoScrollProperty, enabled);
}
/// <summary>
/// Getter for AutoScrollProperty
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static bool GetAutoScroll(UIElement element) {
return (bool)element.GetValue(AutoScrollProperty);
}
/// <summary>
/// Triggered when AutoScroll property changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
public static void OnAutoScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var textBox = d as TextBox;
if (textBox == null) return;
textBox.SelectionChanged -= UpdateScrollOnSelectionChanged;
if ((bool)e.NewValue == true) {
textBox.SelectionChanged += UpdateScrollOnSelectionChanged;
}
}
/// <summary>
/// Triggered when placeholder changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var textBox = d as TextBox;
if (textBox != null) textBox.Loaded += OnTextLoaded;
}
/// <summary>
/// Setter for Placeholder property
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetPlaceholder(UIElement element, string value) {
element.SetValue(PlaceholderProperty, value);
}
/// <summary>
/// Getter for Placeholder property
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static string GetPlaceholder(UIElement element) {
return (string)element.GetValue(PlaceholderProperty);
}
/// <summary>
/// Setter for PlaceholderBaseline property
/// </summary>
/// <param name="element"></param>
/// <param name="align"></param>
public static void SetPlaceholderBaseline(UIElement element, AlignmentY align) {
element.SetValue(PlaceholderBaselineProperty, align);
}
/// <summary>
/// Setter for PlaceholderBaseline property.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static AlignmentY GetPlaceholderBaseline(UIElement element) {
return (AlignmentY)element.GetValue(PlaceholderBaselineProperty);
}
/// <summary>
/// Setter for PlaceholderBrush property.
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetPlaceholderBrush(UIElement element, Brush value) {
element.SetValue(PlaceholderBrushProperty, value);
}
/// <summary>
/// Getter for PlaceholderBrush property.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static Brush GetPlaceholderBrush(UIElement element) {
return (Brush)element.GetValue(PlaceholderBrushProperty);
}
/// <summary>
/// Set placeholder as background when TextBox was loaded.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTextLoaded(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (textBox == null) return;
Label placeholder = new Label();
placeholder.Content = textBox.GetValue(PlaceholderProperty);
VisualBrush brush = new VisualBrush();
brush.AlignmentX = AlignmentX.Left;
brush.AlignmentY = GetPlaceholderBaseline(textBox);
brush.TileMode = TileMode.None;
brush.Stretch = Stretch.None;
brush.Opacity = 0.3;
brush.Visual = placeholder;
textBox.SetValue(PlaceholderBrushProperty, brush);
textBox.Background = brush;
textBox.TextChanged += UpdatePlaceholder;
UpdatePlaceholder(textBox, null);
}
/// <summary>
/// Dynamically hide/show placeholder.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void UpdatePlaceholder(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (string.IsNullOrEmpty(textBox.Text)) {
textBox.Background = textBox.GetValue(PlaceholderBrushProperty) as Brush;
} else {
textBox.Background = Brushes.Transparent;
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void UpdateScrollOnSelectionChanged(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (textBox != null && textBox.IsFocused) {
if (Mouse.LeftButton == MouseButtonState.Pressed && textBox.SelectionLength > 0) {
var p = Mouse.GetPosition(textBox);
if (p.X <= 8) {
textBox.LineLeft();
} else if (p.X >= textBox.ActualWidth - 8) {
textBox.LineRight();
}
if (p.Y <= 8) {
textBox.LineUp();
} else if (p.Y >= textBox.ActualHeight - 8) {
textBox.LineDown();
}
} else {
var rect = textBox.GetRectFromCharacterIndex(textBox.CaretIndex);
if (rect.Left <= 0) {
textBox.ScrollToHorizontalOffset(textBox.HorizontalOffset + rect.Left);
} else if (rect.Right >= textBox.ActualWidth) {
textBox.ScrollToHorizontalOffset(textBox.HorizontalOffset + rect.Right);
}
if (rect.Top <= 0) {
textBox.ScrollToVerticalOffset(textBox.VerticalOffset + rect.Top);
} else if (rect.Bottom >= textBox.ActualHeight) {
textBox.ScrollToVerticalOffset(textBox.VerticalOffset + rect.Bottom);
}
}
}
}
}
}

View file

@ -0,0 +1,329 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Helper class to enable multi-selection of TreeView
/// </summary>
public static class TreeViewHelper {
/// <summary>
/// Definition of EnableMultiSelection property.
/// </summary>
public static readonly DependencyProperty EnableMultiSelectionProperty =
DependencyProperty.RegisterAttached(
"EnableMultiSelection",
typeof(bool),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(false, OnEnableMultiSelectionChanged));
/// <summary>
/// Getter of EnableMultiSelection
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetEnableMultiSelection(DependencyObject obj) {
return (bool)obj.GetValue(EnableMultiSelectionProperty);
}
/// <summary>
/// Setter of EnableMultiSelection
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetEnableMultiSelection(DependencyObject obj, bool value) {
obj.SetValue(EnableMultiSelectionProperty, value);
}
/// <summary>
/// Definition of SelectedItems
/// </summary>
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached(
"SelectedItems",
typeof(ObservableCollection<TreeViewItem>),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Getter of SelectedItems
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static ObservableCollection<TreeViewItem> GetSelectedItems(DependencyObject obj) {
return (ObservableCollection<TreeViewItem>)obj.GetValue(SelectedItemsProperty);
}
/// <summary>
/// Setter of SelectedItems
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetSelectedItems(DependencyObject obj, ObservableCollection<TreeViewItem> value) {
obj.SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Definition of IsChecked property.
/// </summary>
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.RegisterAttached(
"IsChecked",
typeof(bool),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(false));
/// <summary>
/// Getter of IsChecked Property.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetIsChecked(DependencyObject obj) {
return (bool)obj.GetValue(IsCheckedProperty);
}
/// <summary>
/// Setter of IsChecked property
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetIsChecked(DependencyObject obj, bool value) {
obj.SetValue(IsCheckedProperty, value);
}
/// <summary>
/// Definition of MultiSelectionChangedEvent
/// </summary>
public static readonly RoutedEvent MultiSelectionChangedEvent =
EventManager.RegisterRoutedEvent("MultiSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeViewHelper));
/// <summary>
/// Add handler for MultiSelectionChanged event.
/// </summary>
/// <param name="d"></param>
/// <param name="handler"></param>
public static void AddMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
var tree = d as TreeView;
if (tree != null) tree.AddHandler(MultiSelectionChangedEvent, handler);
}
/// <summary>
/// Remove handler for MultiSelectionChanged event.
/// </summary>
/// <param name="d"></param>
/// <param name="handler"></param>
public static void RemoveMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
var tree = d as TreeView;
if (tree != null) tree.RemoveHandler(MultiSelectionChangedEvent, handler);
}
/// <summary>
/// Select all items in tree.
/// </summary>
/// <param name="tree"></param>
public static void SelectWholeTree(TreeView tree) {
var selected = GetSelectedItems(tree);
selected.Clear();
SelectAll(selected, tree);
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Selected one item by DataContext
/// </summary>
/// <param name="tree"></param>
/// <param name="obj"></param>
public static void SelectOneByContext(TreeView tree, object obj) {
var item = FindTreeViewItemByDataContext(tree, obj);
if (item != null) {
var selected = GetSelectedItems(tree);
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
}
/// <summary>
/// Unselect the whole tree.
/// </summary>
/// <param name="tree"></param>
public static void UnselectTree(TreeView tree) {
var selected = GetSelectedItems(tree);
if (selected.Count == 0) return;
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Hooks when EnableMultiSelection changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnEnableMultiSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var tree = d as TreeView;
if (tree != null && (bool)e.NewValue) {
tree.SetValue(SelectedItemsProperty, new ObservableCollection<TreeViewItem>());
tree.PreviewMouseDown += OnTreeMouseDown;
}
}
/// <summary>
/// Preview mouse button select.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTreeMouseDown(object sender, MouseButtonEventArgs e) {
var tree = sender as TreeView;
if (tree == null) return;
var hit = VisualTreeHelper.HitTest(tree, e.GetPosition(tree));
if (hit == null || hit.VisualHit is null) return;
var item = FindTreeViewItem(hit.VisualHit as UIElement);
if (item == null) return;
var selected = GetSelectedItems(tree);
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
if (GetIsChecked(item)) {
selected.Remove(item);
item.SetValue(IsCheckedProperty, false);
} else {
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
}
} else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && selected.Count > 0) {
var last = selected.Last();
if (last == item) return;
var lastPos = last.PointToScreen(new Point(0, 0));
var curPos = item.PointToScreen(new Point(0, 0));
if (lastPos.Y > curPos.Y) {
SelectRange(selected, tree, item, last);
} else {
SelectRange(selected, tree, last, item);
}
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
} else if (e.RightButton == MouseButtonState.Pressed) {
if (GetIsChecked(item)) return;
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
} else {
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
}
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Find TreeViewItem by child element.
/// </summary>
/// <param name="item"></param>
/// <param name="child"></param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItem(DependencyObject child) {
if (child == null) return null;
if (child is TreeViewItem) return child as TreeViewItem;
if (child is TreeView) return null;
return FindTreeViewItem(VisualTreeHelper.GetParent(child));
}
/// <summary>
/// Find TreeViewItem by DataContext
/// </summary>
/// <param name="control"></param>
/// <param name="obj"></param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl control, object obj) {
if (control == null) return null;
if (control.DataContext == obj) return control as TreeViewItem;
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
var found = FindTreeViewItemByDataContext(child, obj);
if (found != null) return found;
}
return null;
}
/// <summary>
/// Select all items.
/// </summary>
/// <param name="selected"></param>
/// <param name="control"></param>
private static void SelectAll(ObservableCollection<TreeViewItem> selected, ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
SelectAll(selected, child);
}
}
/// <summary>
/// Select range items between given.
/// </summary>
/// <param name="selected"></param>
/// <param name="control"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="started"></param>
private static int SelectRange(ObservableCollection<TreeViewItem> selected, ItemsControl control, TreeViewItem from, TreeViewItem to, int matches = 0) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
if (matches == 1) {
if (child == to) return 2;
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
if (TryEndRangeSelection(selected, child, to)) return 2;
} else if (child == from) {
matches = 1;
if (TryEndRangeSelection(selected, child, to)) return 2;
} else {
matches = SelectRange(selected, child, from, to, matches);
if (matches == 2) return 2;
}
}
return matches;
}
private static bool TryEndRangeSelection(ObservableCollection<TreeViewItem> selected, TreeViewItem control, TreeViewItem end) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
if (child == end) {
return true;
} else {
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
var ended = TryEndRangeSelection(selected, child, end);
if (ended) return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,136 @@
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Controls;
namespace SourceGit.Helpers {
/// <summary>
/// Validate clone folder.
/// </summary>
public class CloneFolderRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var badPath = "EXISTS and FULL ACCESS CONTROL needed";
var path = value as string;
return Directory.Exists(path) ? ValidationResult.ValidResult : new ValidationResult(false, badPath);
}
}
/// <summary>
/// Validate git remote URL
/// </summary>
public class RemoteUriRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var badUrl = "Remote git URL not supported";
return Git.Repository.IsValidUrl(value as string) ? ValidationResult.ValidResult : new ValidationResult(false, badUrl);
}
}
/// <summary>
/// Validate tag name.
/// </summary>
public class RemoteNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\.]+$");
var name = value as string;
var remotes = Repo.Remotes();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Remote name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for remote. Regex: ^[\\w\\-\\.]+$");
foreach (var t in remotes) {
if (t.Name == name) {
return new ValidationResult(false, $"Remote '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Validate branch name.
/// </summary>
public class BranchNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public string Prefix { get; set; } = "";
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-/\.]+$");
var name = value as string;
var branches = Repo.Branches();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Branch name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for branch. Regex: ^[\\w\\-/\\.]+$");
name = Prefix + name;
foreach (var b in branches) {
if (b.Name == name) {
return new ValidationResult(false, $"Branch '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Validate tag name.
/// </summary>
public class TagNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\.]+$");
var name = value as string;
var tags = Repo.Tags();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Tag name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for tag. Regex: ^[\\w\\-\\.]+$");
foreach (var t in tags) {
if (t.Name == name) {
return new ValidationResult(false, $"Tag '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for commit subject.
/// </summary>
public class CommitSubjectRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var subject = value as string;
return string.IsNullOrWhiteSpace(subject) ? new ValidationResult(false, "Commit subject can NOT be empty") : ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for patch file.
/// </summary>
public class PatchFileRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var path = value as string;
var succ = !string.IsNullOrEmpty(path) && File.Exists(path);
return !succ ? new ValidationResult(false, "Invalid path for patch file") : ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for submodule path.
/// </summary>
public class SubmodulePathRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\._/]+$");
var path = value as string;
var succ = !string.IsNullOrEmpty(path) && regex.IsMatch(path.Trim());
return !succ ? new ValidationResult(false, "Invalid path for submodules") : ValidationResult.ValidResult;
}
}
}

View file

@ -0,0 +1,3 @@
using System.Windows;
[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]

View file

@ -0,0 +1,22 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Border.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Button.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/CheckBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ComboBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ContextMenu.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/DataGrid.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/HyperLink.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Label.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ListView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Path.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/RadioButton.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ScrollBar.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ScrollViewer.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TabControl.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TextBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ToggleButton.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Tooltip.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TreeView.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -0,0 +1,61 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Geometry x:Key="Icon.Git">M1004.824 466.4L557.72 19.328c-25.728-25.76-67.488-25.76-93.28 0L360.568 123.2l78.176 78.176c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96 0 14.816-3.36 28.864-9.376 41.376l127.968 127.968c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96s-42.976 96-96 96-96-42.976-96-96c0-14.816 3.36-28.864 9.376-41.376L521.496 374.624a88.837 88.837 0 0 1-9.376 3.872v266.976c37.28 13.184 64 48.704 64 90.528 0 53.024-42.976 96-96 96s-96-42.976-96-96c0-41.792 26.72-77.344 64-90.528V378.496c-37.28-13.184-64-48.704-64-90.528 0-14.816 3.36-28.864 9.376-41.376l-78.176-78.176L19.416 464.288c-25.76 25.792-25.76 67.52 0 93.28l447.136 447.072c25.728 25.76 67.488 25.76 93.28 0l444.992-444.992c25.76-25.76 25.76-67.552 0-93.28z</Geometry>
<Geometry x:Key="Icon.Submodule">M557.696 545.347L789.873 402.66c23.998-14.999 31.297-46.496 16.398-70.493-14.798-23.798-45.995-31.197-69.993-16.699L506.501 456.555 277.123 315.37c-24.098-14.798-55.595-7.3-70.493 16.799-14.799 24.097-7.3 55.594 16.798 70.493l231.778 142.586V819.12c0 28.297 22.897 51.195 51.195 51.195 28.297 0 51.195-22.898 51.195-51.195V545.347h0.1zM506.5 0l443.356 255.975v511.95L506.501 1023.9 63.144 767.925v-511.95L506.5 0z</Geometry>
<Geometry x:Key="Icon.ScrollLeft">M753.613 996.727L269.38 511.505 754.602 27.272z</Geometry>
<Geometry x:Key="Icon.ScrollRight">M270.387 27.273L754.62 512.495 269.398 996.728z</Geometry>
<Geometry x:Key="Icon.Minimize">F1M0,6L0,9 9,9 9,6 0,6z</Geometry>
<Geometry x:Key="Icon.Maximize">F1M0,0L0,9 9,9 9,0 0,0 0,3 8,3 8,8 1,8 1,3z</Geometry>
<Geometry x:Key="Icon.Restore">F1M0,10L0,3 3,3 3,0 10,0 10,2 4,2 4,3 7,3 7,6 6,6 6,5 1,5 1,10z M1,10L7,10 7,7 10,7 10,2 9,2 9,6 6,6 6,9 1,9z</Geometry>
<Geometry x:Key="Icon.Close">M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z</Geometry>
<Geometry x:Key="Icon.Check">M512 597.33333332m-1.26648097 0a1.26648097 1.26648097 0 1 0 2.53296194 0 1.26648097 1.26648097 0 1 0-2.53296194 0ZM809.691429 392.777143L732.16 314.514286 447.634286 599.771429 292.571429 443.977143 214.308571 521.508571l155.794286 155.794286 77.531429 77.531429 362.057143-362.057143z</Geometry>
<Geometry x:Key="Icon.Loading">M511.680999 0C233.071131 0 6.524722 222.580887 0.12872 499.655715 6.013042 257.886821 189.834154 63.960025 415.740962 63.960025c229.61649 0 415.740162 200.450718 415.740162 447.720175 0 52.958901 42.981137 95.940037 95.940038 95.940037s95.940037-42.981137 95.940037-95.940037c0-282.57539-229.104809-511.6802-511.6802-511.6802z m0 1023.3604c278.609869 0 505.156277-222.580887 511.55228-499.655715-5.884322 241.768894-189.705434 435.69569-415.612242 435.69569-229.61649 0-415.740162-200.450718-415.740163-447.720175 0-52.958901-42.981137-95.940037-95.940037-95.940038s-95.940037 42.981137-95.940037 95.940038c0 282.57539 229.104809 511.6802 511.680199 511.6802z</Geometry>
<Geometry x:Key="Icon.Search">M701.9062029 677.41589899L589.90712068 565.41681675a148.33953321 148.33953321 0 1 0-24.97646381 26.55648342L676.07895931 703.12160261z m-346.38891409-199.50786053a114.97681148 114.97681148 0 1 1 114.85527151 114.97681148A115.09835147 115.09835147 0 0 1 355.45651882 477.90803846z</Geometry>
<Geometry x:Key="Icon.Conflict">M352 64h320L960 352v320L672 960h-320L64 672v-320L352 64z m161.28 362.688L344.128 256 259.584 341.312 428.736 512l-169.152 170.688L344.128 768 513.28 597.312 682.432 768l84.544-85.312L597.824 512l169.152-170.688L682.432 256 513.28 426.688z</Geometry>
<Geometry x:Key="Icon.List">M51.2 204.8h102.4v102.4H51.2V204.8z m204.8 0h716.8v102.4H256V204.8zM51.2 460.8h102.4v102.4H51.2V460.8z m204.8 0h716.8v102.4H256V460.8z m-204.8 256h102.4v102.4H51.2v-102.4z m204.8 0h716.8v102.4H256v-102.4z</Geometry>
<Geometry x:Key="Icon.Tree">M912 737l0 150L362 887l0-100 0-50 0-150 0-150 0-150L112 287l0-150 450 0 0 150L412 287l0 150L912 437l0 150L412 587l0 150L912 737z</Geometry>
<Geometry x:Key="Icon.MoveUp">M868 545.5L536.1 163c-12.7-14.7-35.5-14.7-48.3 0L156 545.5c-4.5 5.2-0.8 13.2 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z</Geometry>
<Geometry x:Key="Icon.MoveDown">M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861c12.7 14.7 35.5 14.7 48.3 0L868 478.5c4.5-5.2 0.8-13.2-6-13.2z</Geometry>
<Geometry x:Key="Icon.StageSelected">M509.44 546.304l270.848-270.912 90.56 90.56-347.52 349.056-0.832-0.768-13.056 13.056-362.624-361.28 91.136-91.264z</Geometry>
<Geometry x:Key="Icon.StageAll">M256 224l1e-8 115.2L512 544l255.99999999-204.8 1e-8-115.2-256 204.80000001L256 224zM512 684.8l-256-204.8L256 595.2 512 800 768 595.2l0-115.2L512 684.8z</Geometry>
<Geometry x:Key="Icon.UnstageSelected">M169.5 831l342.8-341.9L855.1 831l105.3-105.3-448.1-448.1L64.2 725.7 169.5 831z</Geometry>
<Geometry x:Key="Icon.UnstageAll">M768 800V684.8L512 480 256 684.8V800l256-204.8L768 800zM512 339.2L768 544V428.8L512 224 256 428.8V544l256-204.8z</Geometry>
<Geometry x:Key="Icon.Preference">M64.2 180.3h418.2v120.6H64.2zM64.2 461.7h358.5v120.6H64.2zM64.2 723.1h418.2v120.6H64.2zM601.9 180.3h358.5v120.6H601.9zM482.4 119.9h179.2v241.3H482.4zM303.2 401.4h179.2v241.3H303.2zM482.4 662.8h179.2v241.3H482.4zM540.3 461.7h420.1v120.6H540.3zM601.9 723.1h358.5v120.6H601.9z</Geometry>
<Geometry x:Key="Icon.Setting">M887 576.8v-129.4L796.6 418c-4.6-14-10.2-27.4-16.8-40.4l43.2-84.8-91.6-91.6-84.8 43.2c-13-6.6-26.6-12.2-40.4-16.8l-29.4-90.4h-129.4L418 227.6c-13.8 4.6-27.4 10.2-40.4 16.8l-84.8-43.2-91.6 91.6 43.2 84.8c-6.6 13-12.2 26.6-16.8 40.4l-90.4 29.4v129.4l90.4 29.4c4.6 13.8 10.2 27.4 16.8 40.4l-43.2 84.8 91.6 91.6 84.8-43.2c13 6.6 26.6 12.2 40.4 16.8l29.4 90.4h129.4l29.4-90.4c14-4.6 27.4-10.2 40.4-16.8l84.8 43.2 91.6-91.6-43.2-84.8c6.6-13 12.2-26.6 16.8-40.4l90.4-29.4zM512 662c-82.8 0-150-67.2-150-150s67.2-150 150-150 150 67.2 150 150-67.2 150-150 150z</Geometry>
<Geometry x:Key="Icon.Info">M 38,19C 48.4934,19 57,27.5066 57,38C 57,48.4934 48.4934,57 38,57C 27.5066,57 19,48.4934 19,38C 19,27.5066 27.5066,19 38,19 Z M 33.25,33.25L 33.25,36.4167L 36.4166,36.4167L 36.4166,47.5L 33.25,47.5L 33.25,50.6667L 44.3333,50.6667L 44.3333,47.5L 41.1666,47.5L 41.1666,36.4167L 41.1666,33.25L 33.25,33.25 Z M 38.7917,25.3333C 37.48,25.3333 36.4167,26.3967 36.4167,27.7083C 36.4167,29.02 37.48,30.0833 38.7917,30.0833C 40.1033,30.0833 41.1667,29.02 41.1667,27.7083C 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333 Z</Geometry>
<Geometry x:Key="Icon.Folder">M64 864h896V288h-396.224a64 64 0 0 1-57.242667-35.376L460.224 160H64v704z m-64 32V128a32 32 0 0 1 32-32h448a32 32 0 0 1 28.624 17.690667L563.776 224H992a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H32a32 32 0 0 1-32-32z</Geometry>
<Geometry x:Key="Icon.Folder.Fill">M448 64l128 128h448v768H0V64z</Geometry>
<Geometry x:Key="Icon.Folder.Open">M832 960l192-512H192L0 960zM128 384L0 960V128h288l128 128h416v128z</Geometry>
<Geometry x:Key="Icon.File">M958.656 320H960v639.936A64 64 0 0 1 896.128 1024H191.936A63.872 63.872 0 0 1 128 959.936V64.064A64 64 0 0 1 191.936 0H640v320.96h319.616L958.656 320zM320 544c0 17.152 14.464 32 32.192 32h383.552A32.384 32.384 0 0 0 768 544c0-17.152-14.464-32-32.256-32H352.192A32.448 32.448 0 0 0 320 544z m0 128c0 17.152 14.464 32 32.192 32h383.552a32.384 32.384 0 0 0 32.256-32c0-17.152-14.464-32-32.256-32H352.192a32.448 32.448 0 0 0-32.192 32z m0 128c0 17.152 14.464 32 32.192 32h383.552a32.384 32.384 0 0 0 32.256-32c0-17.152-14.464-32-32.256-32H352.192a32.448 32.448 0 0 0-32.192 32z</Geometry>
<Geometry x:Key="Icon.Diff">M854.2 306.6L611.3 72.9c-6-5.7-13.9-8.9-22.2-8.9H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h277l219 210.6V824c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V329.6c0-8.7-3.5-17-9.8-23zM553.4 201.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v704c0 17.7 14.3 32 32 32h512c17.7 0 32-14.3 32-32V397.3c0-8.5-3.4-16.6-9.4-22.6L553.4 201.4zM568 753c0 3.8-3.4 7-7.5 7h-225c-4.1 0-7.5-3.2-7.5-7v-42c0-3.8 3.4-7 7.5-7h225c4.1 0 7.5 3.2 7.5 7v42z m0-220c0 3.8-3.4 7-7.5 7H476v84.9c0 3.9-3.1 7.1-7 7.1h-42c-3.8 0-7-3.2-7-7.1V540h-84.5c-4.1 0-7.5-3.2-7.5-7v-42c0-3.9 3.4-7 7.5-7H420v-84.9c0-3.9 3.2-7.1 7-7.1h42c3.9 0 7 3.2 7 7.1V484h84.5c4.1 0 7.5 3.1 7.5 7v42z</Geometry>
<Geometry x:Key="Icon.Filter">M599.22969 424.769286 599.22969 657.383158 424.769286 831.844585 424.769286 424.769286 192.155415 192.155415 831.844585 192.155415Z</Geometry>
<Geometry x:Key="Icon.Binary">M71.111111 1024V0h661.333333L952.888889 219.420444V1024H71.111111z m808.305778-731.420444l-220.444445-219.448889H144.583111V950.897778h734.833778V292.579556zM438.528 512h-220.444444V219.420444h220.444444V512z m-73.500444-219.420444H291.555556v146.289777h73.472v-146.289777z m0 512h73.500444v73.130666h-220.444444v-73.130666H291.555556v-146.289778H218.083556V585.102222h146.944v219.448889z m293.944888-365.710223h73.472V512H512v-73.130667h73.472v-146.289777H512V219.420444h146.972444v219.448889z m73.472 438.840889H512V585.130667h220.444444v292.579555z m-73.472-219.420444h-73.500444v146.289778h73.500444v-146.289778z</Geometry>
<Geometry x:Key="Icon.Vertical">M1024 1024H0V0h1024v1024z m-64-64V320H320V256h640V64H64v896h192V64h64v896z</Geometry>
<Geometry x:Key="Icon.Horizontal">M81.92 81.92v860.16h860.16V81.92H81.92z m802.304 57.856V322.56H139.776V139.776h744.448z m-744.448 240.64H322.56v503.808H139.776V380.416z m240.128 503.808V380.416h504.32v503.808H379.904z</Geometry>
<Geometry x:Key="Icon.Fetch">M1024 896v128H0V704h128v192h768V704h128v192zM576 554.688L810.688 320 896 405.312l-384 384-384-384L213.312 320 448 554.688V0h128v554.688z</Geometry>
<Geometry x:Key="Icon.Pull">M432 0h160c26.6 0 48 21.4 48 48v336h175.4c35.6 0 53.4 43 28.2 68.2L539.4 756.6c-15 15-39.6 15-54.6 0L180.2 452.2c-25.2-25.2-7.4-68.2 28.2-68.2H384V48c0-26.6 21.4-48 48-48z m592 752v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h293.4l98 98c40.2 40.2 105 40.2 145.2 0l98-98H976c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z</Geometry>
<Geometry x:Key="Icon.Push">M592 768h-160c-26.6 0-48-21.4-48-48V384h-175.4c-35.6 0-53.4-43-28.2-68.2L484.6 11.4c15-15 39.6-15 54.6 0l304.4 304.4c25.2 25.2 7.4 68.2-28.2 68.2H640v336c0 26.6-21.4 48-48 48z m432-16v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h272v16c0 61.8 50.2 112 112 112h160c61.8 0 112-50.2 112-112v-16h272c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z</Geometry>
<Geometry x:Key="Icon.SaveStash">M961.3 319.6L512 577.3 62.7 319.6 512 62l449.3 257.6zM512 628.4L185.4 441.6 62.7 512 512 769.6 961.3 512l-122.7-70.4L512 628.4zM512 820.8L185.4 634 62.7 704.3 512 962l449.3-257.7L838.6 634 512 820.8z</Geometry>
<Geometry x:Key="Icon.Apply">M295.328 472l143.184 276.032S671.184 186.992 1038.096 0c-8.944 133.568-44.8 249.328 17.904 391.792C894.912 427.408 563.792 828.112 456.4 1024 304.272 837.008 125.296 694.544 0 650.016z</Geometry>
<Geometry x:Key="Icon.Terminal">M89.6 806.4h844.8V217.6H89.6v588.8zM0 128h1024v768H0V128z m242.816 577.536L192 654.72l154.304-154.368L192 346.048l50.816-50.816L448 500.352 242.816 705.536z m584.32 13.248H512V640h315.072v78.72z</Geometry>
<Geometry x:Key="Icon.Flow">M508.928 556.125091l92.904727 148.759273h124.462546l-79.639273-79.173819 49.245091-49.524363 164.584727 163.700363-164.631273 163.002182-49.152-49.617454 79.36-78.568728h-162.955636l-95.650909-153.227636 41.472-65.349818z m186.973091-394.705455l164.584727 163.700364-164.631273 163.002182-49.152-49.617455L726.109091 359.936H529.687273l-135.540364 223.976727H139.636364v-69.818182h215.133091l135.586909-223.976727h235.938909l-79.639273-79.173818 49.245091-49.524364z</Geometry>
<Geometry x:Key="Icon.Commit">M795.968 471.04A291.584 291.584 0 0 0 512 256a293.376 293.376 0 0 0-283.968 215.04H0v144h228.032A292.864 292.864 0 0 0 512 832a291.136 291.136 0 0 0 283.968-216.96H1024V471.04h-228.032M512 688A145.856 145.856 0 0 1 366.016 544 144.576 144.576 0 0 1 512 400c80 0 145.984 63.104 145.984 144A145.856 145.856 0 0 1 512 688</Geometry>
<Geometry x:Key="Icon.WorkingCopy">M0 586.459429l403.968 118.784 497.517714-409.892572-385.536 441.490286-1.609143 250.587428 154.916572-204.580571 278.601143 83.456L1170.285714 36.571429z</Geometry>
<Geometry x:Key="Icon.Histories">M24.356571 512A488.155429 488.155429 0 0 1 512 24.356571 488.155429 488.155429 0 0 1 999.643429 512 488.155429 488.155429 0 0 1 512 999.643429 488.155429 488.155429 0 0 1 24.356571 512z m446.976-325.046857v326.656L242.614857 619.227429l51.126857 110.665142 299.52-138.24V186.953143H471.332571z</Geometry>
<Geometry x:Key="Icon.Stashes">M714.624 253.648h-404.8l-57.808 57.328h520.48z m-491.568 85.984v200.624h578.336V339.632z m404.8 143.296h-28.88v-28.64H425.472v28.64h-28.912v-57.312h231.328v57.312z m-404.8 295.12h578.336V559.36H223.056z m173.504-132.704h231.328v57.328h-28.912v-28.656H425.472v28.656h-28.912v-57.328z</Geometry>
<Geometry x:Key="Icon.Branch">M868.736 144.96a144.64 144.64 0 1 0-289.408 0c0 56.064 32.64 107.008 83.456 130.624-4.928 95.552-76.608 128-201.088 174.592-52.48 19.712-110.336 41.6-159.744 74.432V276.16A144.448 144.448 0 0 0 241.664 0.192a144.64 144.64 0 0 0-144.64 144.768c0 58.24 34.688 108.288 84.352 131.2v461.184a144.32 144.32 0 0 0-84.416 131.2 144.704 144.704 0 1 0 289.472 0 144.32 144.32 0 0 0-83.52-130.688c4.992-95.488 76.672-127.936 201.152-174.592 122.368-45.952 273.792-103.168 279.744-286.784a144.64 144.64 0 0 0 84.928-131.52zM241.664 61.44a83.456 83.456 0 1 1 0 166.912 83.456 83.456 0 0 1 0-166.912z m0 890.56a83.52 83.52 0 1 1 0-167.04 83.52 83.52 0 0 1 0 167.04zM724.032 228.416a83.52 83.52 0 1 1 0-167.04 83.52 83.52 0 0 1 0 167.04z</Geometry>
<Geometry x:Key="Icon.Branch.Add">M896 128h-64V64c0-35.2-28.8-64-64-64s-64 28.8-64 64v64h-64c-35.2 0-64 28.8-64 64s28.8 64 64 64h64v64c0 35.2 28.8 64 64 64s64-28.8 64-64V256h64c35.2 0 64-28.8 64-64s-28.8-64-64-64z m-203.52 307.2C672.64 480.64 628.48 512 576 512H448c-46.72 0-90.24 12.8-128 35.2V372.48C394.24 345.6 448 275.2 448 192c0-106.24-85.76-192-192-192S64 85.76 64 192c0 83.2 53.76 153.6 128 180.48v279.68c-74.24 25.6-128 96.64-128 179.84 0 106.24 85.76 192 192 192s192-85.76 192-192c0-66.56-33.92-124.8-84.48-159.36 22.4-19.84 51.84-32.64 84.48-32.64h128c121.6 0 223.36-85.12 248.96-199.04-18.56 4.48-37.12 7.04-56.96 7.04-26.24 0-51.2-5.12-75.52-12.8zM256 128c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0 768c-35.2 0-64-28.8-64-64s28.8-64 64-64 64 28.8 64 64-28.8 64-64 64z</Geometry>
<Geometry x:Key="Icon.Remote">M901.802667 479.232v-1.024c0-133.461333-111.616-241.664-249.514667-241.664-105.813333 0-195.925333 63.829333-232.448 153.941333-27.989333-20.138667-62.464-32.426667-100.010667-32.426666-75.776 0-139.605333 49.152-159.744 116.053333-51.882667 36.522667-86.016 96.938667-86.016 165.205333 0 111.616 90.453333 201.728 201.728 201.728h503.466667c111.616 0 201.728-90.453333 201.728-201.728 0-65.194667-31.061333-123.221333-79.189333-160.085333z</Geometry>
<Geometry x:Key="Icon.Remote.Add">M363.789474 512h67.368421v107.789474h107.789473v67.368421h-107.789473v107.789473h-67.368421v-107.789473h-107.789474v-67.368421h107.789474v-107.789474z m297.539368-64A106.671158 106.671158 0 0 1 768 554.671158C768 613.578105 719.548632 660.210526 660.210526 660.210526h-107.789473v-53.894737h-107.789474v-107.789473h-94.31579v107.789473h-94.315789c4.311579-21.194105 22.231579-46.807579 43.560421-50.755368l-0.889263-11.560421a74.671158 74.671158 0 0 1 71.248842-74.590316 128.053895 128.053895 0 0 1 238.605474-7.437473 106.172632 106.172632 0 0 1 52.816842-13.972211z</Geometry>
<Geometry x:Key="Icon.Tag">M177.311335 156.116617c-22.478967 4.729721-32.774451 17.336854-36.251645 36.893258-10.080589 56.697303-33.399691 257.604032-13.234419 277.769304l445.342858 445.341834c23.177885 23.177885 60.757782 23.178909 83.935668 0l246.019183-246.019183c23.177885-23.177885 23.177885-60.757782 0-83.935668l-445.341834-445.341834C437.419398 120.463606 231.004211 144.82034 177.311335 156.116617zM331.22375 344.221786c-26.195615 26.195615-68.667939 26.195615-94.863555 0-26.195615-26.195615-26.195615-68.666916 0-94.863555s68.667939-26.195615 94.862531 0C357.418342 275.55487 357.419366 318.02617 331.22375 344.221786z</Geometry>
<Geometry x:Key="Icon.Tag.Add">M682.666667 536.576h-143.701334v-142.336h-142.336V283.306667H238.933333a44.032 44.032 0 0 0-40.96 40.96v170.666666a55.978667 55.978667 0 0 0 14.336 34.133334l320.512 320.512a40.96 40.96 0 0 0 57.685334 0l173.738666-173.738667a40.96 40.96 0 0 0 0-57.685333z m-341.333334-108.544a40.96 40.96 0 1 1 0-57.685333 40.96 40.96 0 0 1 0 57.685333zM649.216 284.330667V141.994667h-68.608v142.336h-142.336v68.266666h142.336v142.336h68.608v-142.336h142.336v-68.266666h-142.336z</Geometry>
</ResourceDictionary>

View file

@ -0,0 +1,12 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Border.Badge" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="9"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Height" Value="18"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource Brush.Badge}"/>
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,55 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 无边框按钮(也是默认样式) -->
<Style x:Key="Style.Button" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="Opacity" Value="0.9"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 修改默认样式 -->
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button}"/>
<!-- 无边框但显示Hover -->
<Style x:Key="Style.Button.HighlightHover" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#40000000"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 有边框 -->
<Style x:Key="Style.Button.Bordered" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
<Style x:Key="Style.Button.AccentBordered" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Setter Property="Background" Value="{DynamicResource Brush.Accent1}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.FG}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,38 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" BorderBrush="{DynamicResource Brush.Border1}" BorderThickness="1" Background="Transparent">
<Path x:Name="Checked" Height="12" Width="12" Style="{DynamicResource Style.Icon}" Data="{DynamicResource Icon.Check}" Fill="{DynamicResource Brush.Accent1}"/>
</Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="8,0,0,0" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter TargetName="Checked" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,83 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTemplate x:Key="Template.ComboBox.ToggleButton" TargetType="{x:Type ToggleButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.ColumnSpan="2" CornerRadius="0" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{DynamicResource Brush.Border1}" Background="Transparent"/>
<Border Grid.Column="0" CornerRadius="0" Margin="1" Background="Transparent"/>
<Path x:Name="Arrow" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z" Fill="{DynamicResource Brush.Border1}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="Template.ComboBox.TextBox" TargetType="{x:Type TextBox}">
<Border x:Name="PART_ContentHost" Focusable="False" Background="{TemplateBinding Background}" />
</ControlTemplate>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="true" />
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="MinWidth" Value="120" />
<Setter Property="MinHeight" Value="20" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton x:Name="ToggleButton" BorderThickness="{TemplateBinding BorderThickness}" Template="{StaticResource Template.ComboBox.ToggleButton}" Grid.Column="2" Focusable="false" ClickMode="Press" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<ContentPresenter x:Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left" TextElement.Foreground="{DynamicResource Brush.FG}"/>
<TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" Template="{StaticResource Template.ComboBox.TextBox}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" />
<Popup x:Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Grid x:Name="DropDown" SnapsToDevicePixels="True" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border x:Name="DropDownBorder" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Background="{DynamicResource Brush.BG2}"/>
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" TextElement.Foreground="{DynamicResource Brush.FG}"/>
</ScrollViewer>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
<Trigger SourceName="Popup" Property="AllowsTransparency" Value="true">
<Setter TargetName="DropDownBorder" Property="CornerRadius" Value="0" />
<Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Border" Padding="2" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,91 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="MinHeight" Value="24"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border Background="{DynamicResource Brush.BG1}" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
<StackPanel IsItemsHost="True" Margin="1" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="{x:Static MenuItem.SubmenuItemTemplateKey}" TargetType="{x:Type MenuItem}">
<Border Name="Border" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="Icon" Grid.Column="0" HorizontalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter Name="HeadHost" Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="{x:Static MenuItem.SubmenuHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
<Border Name="Border" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="Icon" Grid.Column="0" Margin="6,0" VerticalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter Name="HeadHost" Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
<Path Grid.Column="2" Width="8" Height="8" Style="{DynamicResource Style.Icon}" Data="M 0 0 L 0 7 L 4 3.5 Z"/>
<Popup Name="Popup" Placement="Right" HorizontalOffset="-2" IsOpen="{TemplateBinding IsSubmenuOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
<Border Name="SubmenuBorder" SnapsToDevicePixels="True" Background="{DynamicResource Brush.BG1}" BorderBrush="{DynamicResource Brush.Border1}" BorderThickness="1">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="{x:Type Separator}">
<Setter Property="Margin" Value="30,2,0,2"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height=".8" VerticalAlignment="Center" SnapsToDevicePixels="True" Fill="{DynamicResource Brush.Border1}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,44 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.DataGridCell" TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style x:Key="Style.DataGridRow" TargetType="{x:Type DataGridRow}">
<Setter Property="BorderThickness" Value="0"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGrid}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="{x:Static SystemColors.HighlightColor}"/>
</Style.Resources>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="CanUserDeleteRows" Value="False"/>
<Setter Property="CanUserResizeRows" Value="False"/>
<Setter Property="CanUserReorderColumns" Value="False"/>
<Setter Property="CanUserResizeColumns" Value="False" />
<Setter Property="CanUserSortColumns" Value="False"/>
<Setter Property="AllowDrop" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="TextElement.FontFamily" Value="Consolas"/>
<Setter Property="EnableColumnVirtualization" Value="True"/>
<Setter Property="EnableRowVirtualization" Value="True"/>
<Setter Property="RowBackground" Value="Transparent"/>
<Setter Property="HeadersVisibility" Value="None"/>
<Setter Property="GridLinesVisibility" Value="None"/>
<Setter Property="CellStyle" Value="{StaticResource Style.DataGridCell}"/>
<Setter Property="RowStyle" Value="{StaticResource Style.DataGridRow}"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="TextDecorations" Value="{x:Null}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Label" TargetType="{x:Type Label}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource Style.Label}"/>
<Style x:Key="Style.Label.GroupHeader" BasedOn="{StaticResource Style.Label}" TargetType="{x:Type Label}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="Opacity" Value=".5"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,66 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ListViewItem.Borderless" TargetType="{x:Type ListViewItem}">
<Setter Property="Padding" Value="2"/>
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Border x:Name="Border" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ListView.Borderless" TargetType="{x:Type ListView}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="ItemContainerStyle" Value="{StaticResource Style.ListViewItem.Borderless}"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListView}">
<Border Name="Border" BorderThickness="0" Background="{TemplateBinding Background}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}">
<ItemsPresenter/>
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Icon" TargetType="{x:Type Path}">
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Stretch" Value="Uniform"/>
<Setter Property="Fill" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
</Style>
<Style x:Key="Style.WindowControlIcon" TargetType="{x:Type Path}">
<Setter Property="Stretch" Value="None"/>
<Setter Property="Fill" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
<Setter Property="RenderOptions.EdgeMode" Value="Aliased"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,52 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type RadioButton}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Path
x:Name="Border"
Width="14" Height="14"
Stretch="Uniform"
Fill="Transparent"
Stroke="{DynamicResource Brush.Border1}" StrokeThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 A 180,180 180 1 1 1,1 Z"/>
<Path
x:Name="Dot"
Width="10" Height="10"
Stretch="Uniform"
Fill="{DynamicResource Brush.Accent1}"
Stroke="Transparent" StrokeThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 A 180,180 180 1 1 1,1 Z"
Visibility="Collapsed"/>
</Grid>
<Grid Grid.Column="1" Margin="4,0">
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Dot" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Stroke" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,98 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ScrollBar.RepeatPage" TargetType="{x:Type RepeatButton}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border x:Name="area" Background="Transparent" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="area" Property="Background" Value="{DynamicResource Brush.FG}" />
<Setter TargetName="area" Property="Opacity" Value=".08"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ScrollBar.Thumb" TargetType="{x:Type Thumb}">
<Setter Property="Background" Value="{DynamicResource Brush.Border1}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Focusable" Value="false" />
<Setter Property="IsTabStop" Value="false" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border x:Name="Border" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Opacity=".6"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="Template.ScrollBar.Horizontal" TargetType="{x:Type ScrollBar}">
<Grid>
<Track Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource Style.ScrollBar.Thumb}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="Template.ScrollBar.Vertical" TargetType="{x:Type ScrollBar}">
<Grid>
<Track Name="PART_Track"
Grid.Row="1"
IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource Style.ScrollBar.Thumb}"/>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Height" Value="Auto" />
<Setter Property="Template" Value="{StaticResource Template.ScrollBar.Vertical}" />
<Setter Property="Width" Value="8" />
</Trigger>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="8" />
<Setter Property="Template" Value="{StaticResource Template.ScrollBar.Horizontal}" />
<Setter Property="Width" Value="Auto" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,54 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Grid Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter
Cursor="{TemplateBinding Cursor}"
Margin="{TemplateBinding Padding}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ScrollBar x:Name="PART_VerticalScrollBar"
IsTabStop="False"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Grid.Column="1" Grid.Row="0" Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Maximum="{TemplateBinding ScrollableHeight}"
Minimum="0"
Value="{TemplateBinding VerticalOffset}"
Margin="0,-1,-1,-1"/>
<ScrollBar x:Name="PART_HorizontalScrollBar"
IsTabStop="False"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Grid.Column="0" Grid.Row="1" Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Maximum="{TemplateBinding ScrollableWidth}"
Minimum="0"
Value="{TemplateBinding HorizontalOffset}"
Margin="-1,0,-1,-1"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,62 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type TabControl}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TabPanel x:Name="HeaderPanel" Grid.Row="0" IsItemsHost="True" KeyboardNavigation.TabIndex="1" Background="Transparent" />
<Border
x:Name="Border"
Grid.Row="1"
Background="Transparent"
KeyboardNavigation.TabNavigation="Local"
KeyboardNavigation.DirectionalNavigation="Contained"
KeyboardNavigation.TabIndex="2">
<ContentPresenter x:Name="PART_SelectedContentHost" Margin="4" ContentSource="SelectedContent" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border x:Name="Border" Margin="0" BorderThickness="0,0,0,1.1" BorderBrush="Transparent" Opacity=".7">
<ContentPresenter
x:Name="ContentSite"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextElement.Foreground="{DynamicResource Brush.FG}"
TextElement.FontWeight="Bold"
ContentSource="Header"
Margin="8,6"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
<Setter TargetName="Border" Property="Opacity" Value="1"/>
<Setter TargetName="ContentSite" Property="TextElement.Foreground" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,126 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:helpers="clr-namespace:SourceGit.Helpers">
<!-- 错误Tooltip -->
<ControlTemplate x:Key="Template.Validation.Tooltip" TargetType="{x:Type ToolTip}">
<Border x:Name="Root" Margin="5,0,0,0" Opacity="0" Padding="0,0,20,20" RenderTransformOrigin="0,0">
<Border.RenderTransform>
<TranslateTransform x:Name="xform" X="-25" />
</Border.RenderTransform>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="OpenStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" />
<VisualTransition GeneratedDuration="0:0:0.2" To="Open">
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" To="0" Storyboard.TargetProperty="X" Storyboard.TargetName="xform">
<DoubleAnimation.EasingFunction>
<BackEase Amplitude=".3" EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Duration="0:0:0.2" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Closed">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
<VisualState x:Name="Open">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="X" Storyboard.TargetName="xform" />
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<FrameworkElement.Effect>
<DropShadowEffect BlurRadius="11" ShadowDepth="6" Opacity="0.4" />
</FrameworkElement.Effect>
<Border Background="#FFDC000C" BorderThickness="1" BorderBrush="#FFBC000C">
<TextBlock Foreground="White" MaxWidth="250" Margin="8,4,8,4" TextWrapping="Wrap" Text="{Binding [0].ErrorContent}" UseLayoutRounding="false" />
</Border>
</Border>
</ControlTemplate>
<!-- 验证错误模板 -->
<ControlTemplate x:Key="Template.Validation.Error">
<AdornedElementPlaceholder x:Name="Target">
<Border BorderBrush="#FFDB000C" BorderThickness="1" x:Name="root">
<ToolTipService.ToolTip>
<ToolTip x:Name="validationTooltip"
Placement="Right"
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Template="{StaticResource Template.Validation.Tooltip}"
Style="{x:Null}"/>
</ToolTipService.ToolTip>
<Grid Background="Transparent" HorizontalAlignment="Right" Height="12" Width="12" Margin="1,-4,-4,0" VerticalAlignment="Top">
<Path Data="M 1,0 L6,0 A 2,2 90 0 1 8,2 L8,7 z" Fill="#FFDC000C" Margin="1,3,0,0" />
</Grid>
</Border>
</AdornedElementPlaceholder>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=Target, Path=AdornedElement.IsKeyboardFocusWithin, Mode=OneWay}" Value="True" />
<Condition Binding="{Binding ElementName=Target, Path=AdornedElement.(Validation.HasError), Mode=OneWay}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="validationTooltip" Property="IsOpen" Value="True"/>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- 修改默认 -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="CaretBrush" Value="{DynamicResource Brush.FG}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource Template.Validation.Error}"/>
<Setter Property="helpers:TextBoxHelper.AutoScroll" Value="True"/>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy" />
<MenuItem Command="ApplicationCommands.Cut" />
<MenuItem Command="ApplicationCommands.Paste" />
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Background="Transparent"
BorderThickness="0"
IsTabStop="False"
CanContentScroll="False"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<Trigger Property="AcceptsReturn" Value="True">
<Setter TargetName="PART_ContentHost" Property="VerticalAlignment" Value="Top"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,111 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ToggleButton.Expender" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<ContentPresenter/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.Filter" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid>
<Path
x:Name="Icon"
Height="12"
Style="{DynamicResource Style.Icon}"
Fill="Transparent"
Stroke="{DynamicResource Brush.FG2}"
StrokeThickness="1"
Data="{DynamicResource Icon.Filter}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.Orientation" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<Path
x:Name="Icon"
Width="18"
Height="18"
Style="{DynamicResource Style.Icon}"
Fill="{DynamicResource Brush.Border1}"
Data="{DynamicResource Icon.Horizontal}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{DynamicResource Icon.Vertical}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.ListOrTree" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<Path
x:Name="Icon"
Height="12"
Width="12"
Style="{DynamicResource Style.Icon}"
Fill="Transparent"
Stroke="{DynamicResource Brush.FG}"
StrokeThickness=".4"
Data="{DynamicResource Icon.Tree}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{DynamicResource Icon.List}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type ToolTip}">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="HasDropShadow" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolTip}">
<Border
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border1}"
Background="{DynamicResource Brush.BG1}"
Width="Auto"
Height="Auto">
<ContentPresenter Margin="6,4" TextElement.Foreground="{DynamicResource Brush.FG}" HorizontalAlignment="Left" VerticalAlignment="Top" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,201 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers">
<converters:TreeViewItemDepthToMargin x:Key="Converter.TreeViewItemIndent" Indent="19"/>
<Style x:Key="Style.TreeView.ToggleButton" TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="16" Height="16" Margin="1" Background="Transparent">
<Path x:Name="ExpandPath" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1,1,1,1" Fill="{DynamicResource Brush.FG}" Data="M 4 0 L 8 4 L 4 8 Z"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="ExpandPath" Value="M 0 4 L 8 4 L 4 8 Z"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.TreeView.ItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, Mode=OneWay, FallbackValue=Stretch, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, Mode=OneWay, FallbackValue=Center, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border
x:Name="BG"
Background="Transparent"
BorderThickness="0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid
Margin="{Binding Converter={StaticResource Converter.TreeViewItemIndent}, RelativeSource={x:Static RelativeSource.TemplatedParent}}"
VerticalAlignment="Stretch"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ToggleButton
Grid.Column="0"
x:Name="Expander"
Style="{StaticResource Style.TreeView.ToggleButton}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={x:Static RelativeSource.TemplatedParent}, Mode=TwoWay}"
ClickMode="Press"/>
<ContentPresenter
x:Name="PART_Header"
Grid.Column="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="BG" Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.TreeView.MultiSelectionItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, Mode=OneWay, FallbackValue=Stretch, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, Mode=OneWay, FallbackValue=Center, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border
x:Name="BG"
Background="Transparent"
BorderThickness="0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid
Margin="{Binding Converter={StaticResource Converter.TreeViewItemIndent}, RelativeSource={x:Static RelativeSource.TemplatedParent}}"
VerticalAlignment="Stretch"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ToggleButton
Grid.Column="0"
x:Name="Expander"
Style="{StaticResource Style.TreeView.ToggleButton}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={x:Static RelativeSource.TemplatedParent}, Mode=TwoWay}"
ClickMode="Press"/>
<ContentPresenter
x:Name="PART_Header"
Grid.Column="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="helpers:TreeViewHelper.IsChecked" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="BG" Property="IsMouseOver" Value="True"/>
<Condition Property="helpers:TreeViewHelper.IsChecked" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="ItemContainerStyle" Value="{StaticResource Style.TreeView.ItemContainerStyle}"/>
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Standard" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeView}">
<Border Name="Border"
Background="{TemplateBinding Background}"
BorderThickness="0"
CornerRadius="0"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ScrollViewer Padding="{TemplateBinding Padding}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
Focusable="False"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,17 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Brush.BG1" Color="#FF252525"/>
<SolidColorBrush x:Key="Brush.BG2" Color="#FF1B1B1B"/>
<SolidColorBrush x:Key="Brush.BG3" Color="#FF202020"/>
<SolidColorBrush x:Key="Brush.BG4" Color="#FF303030"/>
<SolidColorBrush x:Key="Brush.BG5" Color="#FF505050"/>
<SolidColorBrush x:Key="Brush.BG6" Color="#FF404040"/>
<SolidColorBrush x:Key="Brush.Border1" Color="#FF7C7C7C"/>
<SolidColorBrush x:Key="Brush.Border2" Color="#FF404040"/>
<SolidColorBrush x:Key="Brush.FG" Color="#FFF1F1F1"/>
<SolidColorBrush x:Key="Brush.FG2" Color="#40F1F1F1"/>
<SolidColorBrush x:Key="Brush.Badge" Color="#FF8F8F8F"/>
<SolidColorBrush x:Key="Brush.Accent1" Color="#FF007ACC"/>
<SolidColorBrush x:Key="Brush.Accent2" Color="#4C007ACC"/>
</ResourceDictionary>

View file

@ -0,0 +1,16 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Brush.BG1" Color="#FFEEEEF2"/>
<SolidColorBrush x:Key="Brush.BG2" Color="White"/>
<SolidColorBrush x:Key="Brush.BG3" Color="WhiteSmoke"/>
<SolidColorBrush x:Key="Brush.BG4" Color="#FFE6E7E8"/>
<SolidColorBrush x:Key="Brush.BG5" Color="#FFBDBDBD"/>
<SolidColorBrush x:Key="Brush.BG6" Color="#FFCFCFCF"/>
<SolidColorBrush x:Key="Brush.Border1" Color="#FF898989"/>
<SolidColorBrush x:Key="Brush.Border2" Color="#FFCFCFCF"/>
<SolidColorBrush x:Key="Brush.FG" Color="#FF1F1F1F"/>
<SolidColorBrush x:Key="Brush.FG2" Color="DarkGray"/>
<SolidColorBrush x:Key="Brush.Badge" Color="#FF8F8F8F"/>
<SolidColorBrush x:Key="Brush.Accent1" Color="#FF4295FF"/>
<SolidColorBrush x:Key="Brush.Accent2" Color="#4C007ACC"/>
</ResourceDictionary>

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>App.ico</ApplicationIcon>
<Company>sourcegit</Company>
<Description>OpenSource GIT client for Windows</Description>
<Copyright>Copyright © sourcegit 2020. All rights reserved.</Copyright>
<ApplicationManifest>App.manifest</ApplicationManifest>
<Version>1.5</Version>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Resource Include="App.ico" />
</ItemGroup>
</Project>

82
SourceGit/UI/About.xaml Normal file
View file

@ -0,0 +1,82 @@
<Window x:Class="SourceGit.UI.About"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="280" Width="400"
Title="About"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<!-- Enable WindowChrome Feature -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Layout -->
<Border Background="{StaticResource Brush.BG1}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Titlebar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- LOGO -->
<Path Width="20" Height="20" Margin="6,-1,2,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Info}"/>
<!-- Title -->
<Label Grid.Column="1" Content="ABOUT" FontWeight="Light"/>
<!-- Close Button -->
<Button Click="Quit" Width="32" Grid.Column="3" WindowChrome.IsHitTestVisibleInChrome="True">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="90"/>
<RowDefinition Height="40"/>
<RowDefinition Height="32"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,6,0,0">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}" Fill="#FFF05133"/>
</StackPanel>
<Label Grid.Row="1" Content="SourceGit - OPEN SOURCE GIT CLIENT" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" FontSize="18" FontWeight="Bold"/>
<Label Grid.Row="2" Content="{Binding ElementName=me, Path=Version}" HorizontalContentAlignment="Center" FontSize="11"/>
<Label Grid.Row="3" HorizontalContentAlignment="Center" FontSize="11">
<Hyperlink RequestNavigate="OpenSource" NavigateUri="https://gitee.com/sourcegit/SourceGit.git">
<Run Text="https://gitee.com/sourcegit/SourceGit.git"/>
</Hyperlink>
</Label>
<Label Grid.Row="4" Content="Copyright © sourcegit 2020. All rights reserved." HorizontalContentAlignment="Center" FontSize="11"/>
</Grid>
</Grid>
</Border>
</Window>

View file

@ -0,0 +1,46 @@
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// About dialog
/// </summary>
public partial class About : Window {
/// <summary>
/// Current app version
/// </summary>
public string Version {
get {
return "VERSION : " + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion;
}
}
/// <summary>
/// Constructor
/// </summary>
public About() {
InitializeComponent();
}
/// <summary>
/// Open source code link
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OpenSource(object sender, RequestNavigateEventArgs e) {
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
/// <summary>
/// Close this dialog
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
}
}

View file

@ -0,0 +1,72 @@
<UserControl x:Class="SourceGit.UI.AddSubmodule"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="192" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Add Submodule"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="URL :"/>
<TextBox x:Name="txtRepoUrl" Grid.Row="2" Grid.Column="1"
Height="24"
helpers:TextBoxHelper.Placeholder="Git Repository URL">
<TextBox.Text>
<Binding Path="RepoURL" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:RemoteUriRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Parent Folder :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="txtPath"
Height="24"
helpers:TextBoxHelper.Placeholder="Relative foler to store this module. Optional.">
<TextBox.Text>
<Binding Path="LocalPath" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:SubmodulePathRequiredRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<CheckBox Grid.Row="4" Grid.Column="1"
x:Name="chkRecursive"
IsChecked="True"
Content="Fetch nested submodules"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,75 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Dialog to add new submodule.
/// </summary>
public partial class AddSubmodule : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Submodule's repository URL.
/// </summary>
public string RepoURL { get; set; }
/// <summary>
/// Submodule's relative path.
/// </summary>
public string LocalPath { get; set; }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
public AddSubmodule(Git.Repository opened) {
repo = opened;
InitializeComponent();
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
var popup = App.GetPopupManager(repo);
popup?.Show(new AddSubmodule(repo));
}
#region EVENTS
private void SelectFolder(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Select Folder To Clone Repository";
dialog.SelectedPath = repo.Path;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
txtPath.Text = dialog.SelectedPath;
}
}
private async void Sure(object sender, RoutedEventArgs e) {
txtRepoUrl.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtRepoUrl)) return;
txtPath.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtPath)) return;
var recursive = chkRecursive.IsChecked == true;
var popup = App.GetPopupManager(repo);
popup?.Lock();
await Task.Run(() => repo.AddSubmodule(RepoURL, LocalPath, recursive, msg => {
popup?.UpdateStatus(msg);
}));
popup?.Close(true);
}
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
#endregion
}
}

88
SourceGit/UI/Apply.xaml Normal file
View file

@ -0,0 +1,88 @@
<UserControl x:Class="SourceGit.UI.Apply"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
Height="192" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</Grid.Resources>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Apply Patch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Patch File :"/>
<Grid Grid.Row="2" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
x:Name="txtPatchFile"
Height="24"
helpers:TextBoxHelper.Placeholder="Select .patch file to apply">
<TextBox.Text>
<Binding Path="PatchFile" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:PatchFileRequiredRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Column="1" Width="24" Height="24" Click="FindPatchFile" Padding="0" BorderThickness="1" Style="{StaticResource Style.Button.Bordered}">
<Path Width="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Whitespace :"/>
<ComboBox x:Name="combWhitespaceOptions" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center" IsEnabled="{Binding ElementName=chkIgnoreWhitespace, Path=IsChecked, Converter={StaticResource InverseBool}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="20">
<Label Content="{Binding Name}" Padding="4,0"/>
<Label Content="{Binding Desc}" Foreground="{StaticResource Brush.FG2}" FontSize="11" Padding="4,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Grid.Row="4" Grid.Column="1"
x:Name="chkIgnoreWhitespace"
IsChecked="True"
Content="Ignore whitespace changes"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

105
SourceGit/UI/Apply.xaml.cs Normal file
View file

@ -0,0 +1,105 @@
using Microsoft.Win32;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Apply patch dialog
/// </summary>
public partial class Apply : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Whitespace option.
/// </summary>
public class WhitespaceOption {
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public WhitespaceOption(string n, string d, string a) {
Name = n;
Desc = d;
Arg = a;
}
}
/// <summary>
/// Path of file to be patched.
/// </summary>
public string PatchFile { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public Apply(Git.Repository opened) {
repo = opened;
InitializeComponent();
combWhitespaceOptions.ItemsSource = new WhitespaceOption[] {
new WhitespaceOption("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
new WhitespaceOption("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
new WhitespaceOption("Error", "Raise errors and refuses to apply the patch", "error"),
new WhitespaceOption("Error All", "Similar to 'error', but shows more", "error-all"),
};
combWhitespaceOptions.SelectedIndex = 0;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="opened"></param>
public static void Show(Git.Repository opened) {
var popup = App.GetPopupManager(opened);
popup?.Show(new Apply(opened));
}
/// <summary>
/// Open file browser dialog for select a file to patch.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FindPatchFile(object sender, RoutedEventArgs e) {
var dialog = new OpenFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = "Select Patch File";
dialog.InitialDirectory = repo.Path;
dialog.CheckFileExists = true;
if (dialog.ShowDialog() == true) {
PatchFile = dialog.FileName;
txtPatchFile.Text = dialog.FileName;
}
}
/// <summary>
/// Start apply selected path.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtPatchFile.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtPatchFile)) return;
var popup = App.GetPopupManager(repo);
popup?.Lock();
var mode = combWhitespaceOptions.SelectedItem as WhitespaceOption;
var ignoreSpaceChanges = chkIgnoreWhitespace.IsChecked == true;
await Task.Run(() => repo.Apply(PatchFile, ignoreSpaceChanges, mode.Arg));
popup?.Close(true);
}
/// <summary>
/// Cancel options.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

208
SourceGit/UI/Blame.xaml Normal file
View file

@ -0,0 +1,208 @@
<Window x:Class="SourceGit.UI.Blame"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Blame"
Height="600" Width="800">
<!-- Enable WindowChrome -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Content -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Logo & TITLE -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Path
Width="20" Height="20" Margin="6,-1,2,0"
Style="{StaticResource Style.Icon}"
Data="{StaticResource Icon.Git}"
Fill="#FFF05133"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT - BLAME" FontWeight="Light"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Blame file -->
<Border Grid.Row="1" Padding="2,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="blameFile" HorizontalAlignment="Left" FontSize="11" Foreground="{StaticResource Brush.FG2}" FontFamily="Consolas"/>
<Label Grid.Column="1" HorizontalAlignment="Right" Foreground="{StaticResource Brush.FG2}" FontSize="11" Content="Use right mouse button to view commit information."/>
</Grid>
</Border>
<!-- Content -->
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" ClipToBounds="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="lineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
BorderThickness="0"
Background="Transparent"
IsReadOnly="True"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"
FontFamily="Consolas"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="content"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="SyncScrollChanged"
PreviewMouseWheel="MouseWheelOnContent"
SizeChanged="ContentSizeChanged"
SelectionChanged="ContentSelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas">
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy"/>
</ContextMenu>
</RichTextBox.ContextMenu>
<FlowDocument PageWidth="0"/>
</RichTextBox>
<!-- Loading tip -->
<Path x:Name="loading" Grid.ColumnSpan="5" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
<!-- Popup to show commit info -->
<Popup x:Name="popup" Grid.ColumnSpan="5" Placement="MousePoint" IsOpen="False" StaysOpen="False" Focusable="True">
<Border BorderBrush="{StaticResource Brush.Accent1}" BorderThickness="1" Background="{StaticResource Brush.BG1}">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="COMMIT SHA" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="commitID"/>
<Label Grid.Row="1" Grid.Column="0" Content="AUTHOR" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="1" Grid.Column="1" x:Name="authorName"/>
<Label Grid.Row="2" Grid.Column="0" Content="MODIFY TIME" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="2" Grid.Column="1" x:Name="authorTime"/>
</Grid>
</Border>
</Popup>
</Grid>
</Border>
</Grid>
</Border>
</Window>

240
SourceGit/UI/Blame.xaml.cs Normal file
View file

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Viewer to show git-blame
/// </summary>
public partial class Blame : Window {
/// <summary>
/// Background color for blocks.
/// </summary>
public static Brush[] BG = new Brush[] {
Brushes.Transparent,
new SolidColorBrush(Color.FromArgb(128, 0, 0, 0))
};
/// <summary>
/// Constructor
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <param name="revision"></param>
public Blame(Git.Repository repo, string file, string revision) {
InitializeComponent();
double minWidth = content.ActualWidth;
// Move to center.
var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5;
Top = parent.Top + (parent.Height - Height) * 0.5;
// Show loading.
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
loading.Visibility = Visibility.Visible;
// Layout content
blameFile.Content = $"{file}@{revision.Substring(0, 8)}";
Task.Run(() => {
var blame = repo.BlameFile(file, revision);
Dispatcher.Invoke(() => {
content.Document.Blocks.Clear();
if (blame.IsBinary) {
lineNumber.Text = "0";
Paragraph p = new Paragraph(new Run("BINARY FILE BLAME NOT SUPPORTED!!!"));
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
content.Document.Blocks.Add(p);
} else {
List<string> numbers = new List<string>();
for (int i = 0; i < blame.LineCount; i++) numbers.Add(i.ToString());
lineNumber.Text = string.Join("\n", numbers);
numbers.Clear();
for (int i = 0; i < blame.Blocks.Count; i++) {
var frag = blame.Blocks[i];
var idx = i;
Paragraph p = new Paragraph(new Run(frag.Content));
p.DataContext = frag;
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = BG[i % 2];
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
p.ContextMenuOpening += (sender, ev) => {
if (!content.Selection.IsEmpty) return;
Hyperlink link = new Hyperlink(new Run(frag.CommitSHA));
link.ToolTip = "CLICK TO GO";
link.Click += (o, e) => {
repo.OnNavigateCommit?.Invoke(frag.CommitSHA);
e.Handled = true;
};
foreach (var block in content.Document.Blocks) {
var paragraph = block as Paragraph;
if ((paragraph.DataContext as Git.Blame.Block).CommitSHA == frag.CommitSHA) {
paragraph.Background = Brushes.Green;
} else {
paragraph.Background = BG[i % 2];
}
}
commitID.Content = link;
authorName.Content = frag.Author;
authorTime.Content = frag.Time;
popup.IsOpen = true;
ev.Handled = true;
};
var formatter = new FormattedText(
frag.Content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(content.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
content.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) {
content.Document.PageWidth = formatter.Width + 16;
minWidth = formatter.Width;
}
content.Document.Blocks.Add(p);
}
}
// Hide loading.
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed;
});
});
}
/// <summary>
/// Click logo
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
/// <summary>
/// Minimize
/// </summary>
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
/// <summary>
/// Maximize/Restore
/// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
/// <summary>
/// Quit
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
/// <summary>
/// Sync scroll
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SyncScrollChanged(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
var margin = new Thickness(4, -e.VerticalOffset, 4, 0);
lineNumber.Margin = margin;
}
}
/// <summary>
/// Mouse wheel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MouseWheelOnContent(object sender, MouseWheelEventArgs e) {
if (e.Delta > 0) {
content.LineUp();
} else {
content.LineDown();
}
e.Handled = true;
}
/// <summary>
/// Content size changed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentSizeChanged(object sender, SizeChangedEventArgs e) {
if (content.Document.PageWidth < content.ActualWidth) {
content.Document.PageWidth = content.ActualWidth;
}
}
/// <summary>
/// Auto scroll when selection changed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentSelectionChanged(object sender, RoutedEventArgs e) {
var doc = sender as RichTextBox;
if (doc == null || doc.IsFocused == false) return;
if (Mouse.LeftButton == MouseButtonState.Pressed && !doc.Selection.IsEmpty) {
var p = Mouse.GetPosition(doc);
if (p.X <= 8) {
doc.LineLeft();
} else if (p.X >= doc.ActualWidth - 8) {
doc.LineRight();
}
if (p.Y <= 8) {
doc.LineUp();
} else if (p.Y >= doc.ActualHeight - 8) {
doc.LineDown();
}
}
}
}
}

View file

@ -0,0 +1,45 @@
<UserControl x:Class="SourceGit.UI.CherryPick"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Cherry Pick"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Commit :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}" Margin="4,0"/>
<Label x:Name="desc"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="chkCommitChanges" IsChecked="True" Content="Commit the changes"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,58 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Cherry pick commit dialog.
/// </summary>
public partial class CherryPick : UserControl {
private Git.Repository repo = null;
private string commitSHA = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="commit"></param>
public CherryPick(Git.Repository opened, Git.Commit commit) {
InitializeComponent();
repo = opened;
commitSHA = commit.SHA;
desc.Content = $"{commit.ShortSHA} {commit.Subject}";
}
/// <summary>
/// Display this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var popup = App.GetPopupManager(repo);
popup?.Show(new CherryPick(repo, commit));
}
/// <summary>
/// Start pick.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
repo.CherryPick(commitSHA, chkCommitChanges.IsChecked != true);
var popup = App.GetPopupManager(repo);
popup?.Close();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
var popup = App.GetPopupManager(repo);
popup?.Close();
}
}
}

95
SourceGit/UI/Clone.xaml Normal file
View file

@ -0,0 +1,95 @@
<UserControl x:Class="SourceGit.UI.Clone"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
Width="500" Height="224">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Clone Remote Repository"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Repository URL :"/>
<TextBox x:Name="txtUrl" Grid.Row="2" Grid.Column="1"
Height="24"
helpers:TextBoxHelper.Placeholder="Git Repository URL">
<TextBox.Text>
<Binding Path="RemoteUri" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:RemoteUriRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Parent Folder :"/>
<Grid Grid.Row="3" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
x:Name="txtParentFolder"
Height="24"
helpers:TextBoxHelper.Placeholder="Folder to contain this repository">
<TextBox.Text>
<Binding Path="ParentFolder" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:CloneFolderRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Column="1" Width="24" Height="24" Padding="0" BorderThickness="1" Click="SelectParentFolder" Style="{StaticResource Style.Button.Bordered}">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" Content="Local Name :"/>
<TextBox Grid.Row="4" Grid.Column="1"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Repository name. Optional."
Text="{Binding LocalName, ElementName=me, Mode=TwoWay}">
</TextBox>
<Label Grid.Row="5" Grid.Column="0" HorizontalAlignment="Right" Content="Remote Name :"/>
<TextBox Grid.Row="5" Grid.Column="1"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Remote name. Optional."
Text="{Binding RemoteName, ElementName=me, Mode=TwoWay}">
</TextBox>
<Grid Grid.Row="7" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

119
SourceGit/UI/Clone.xaml.cs Normal file
View file

@ -0,0 +1,119 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Clone dialog.
/// </summary>
public partial class Clone : UserControl {
/// <summary>
/// Remote repository
/// </summary>
public string RemoteUri { get; set; }
/// <summary>
/// Parent folder.
/// </summary>
public string ParentFolder { get; set; }
/// <summary>
/// Local name.
/// </summary>
public string LocalName { get; set; }
/// <summary>
/// Remote name.
/// </summary>
public string RemoteName { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public Clone() {
ParentFolder = App.Preference.GitDefaultCloneDir;
InitializeComponent();
}
/// <summary>
/// Show clone dialog.
/// </summary>
public static void Show() {
var popup = App.GetPopupManager(null);
popup?.Show(new Clone());
}
/// <summary>
/// Select parent folder.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SelectParentFolder(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Git Repository URL";
dialog.RootFolder = Environment.SpecialFolder.MyComputer;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
txtParentFolder.Text = dialog.SelectedPath;
}
}
/// <summary>
/// Start clone
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtUrl.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtUrl)) return;
txtParentFolder.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtParentFolder)) return;
string repoName;
if (string.IsNullOrWhiteSpace(LocalName)) {
var from = RemoteUri.LastIndexOfAny(new char[] { '\\', '/' });
if (from <= 0) return;
var name = RemoteUri.Substring(from + 1);
repoName = name.Replace(".git", "");
} else {
repoName = LocalName;
}
string rName;
if (string.IsNullOrWhiteSpace(RemoteName)){
rName = null;
} else {
rName = RemoteName;
}
var popup = App.GetPopupManager(null);
popup.Lock();
var repo = await Task.Run(() => {
return Git.Repository.Clone(RemoteUri, ParentFolder, rName, repoName, popup.UpdateStatus);
});
if (repo == null) {
popup.Unlock();
} else {
popup.Close(true);
repo.Open();
}
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(null).Close();
}
}
}

View file

@ -0,0 +1,420 @@
<UserControl x:Class="SourceGit.UI.CommitViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:source="clr-namespace:SourceGit"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<TabControl>
<TabItem Header="INFORMATION">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition x:Name="committerRow" Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- SHA -->
<Label Grid.Row="0" Grid.Column="0" Content="SHA" HorizontalAlignment="Right" Opacity=".6"/>
<TextBox Grid.Row="0" Grid.Column="1"
x:Name="SHA"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- Refs -->
<Label x:Name="lblRefs" Grid.Row="0" Grid.Column="2" Content="REFS" HorizontalAlignment="Right" Opacity=".6"/>
<ItemsControl Grid.Row="0" Grid.Column="3" x:Name="refs" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="BG" Height="16" Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.BG5}">
<Path x:Name="Icon" Width="8" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
</Border>
<Label x:Name="Name" Grid.Column="1" Content="{Binding Name}" FontSize="11" Padding="4,0" Foreground="Black"/>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.Tag}">
<Setter TargetName="BG" Property="Background" Value="#FF02C302"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Tag}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.LocalBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Branch}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.RemoteBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.CurrentBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
<Setter TargetName="Icon" Property="Fill" Value="Orange"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- PARENTS -->
<Label Grid.Row="1" Grid.Column="0" Content="PARENTS" HorizontalAlignment="Right" Opacity=".6"/>
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" x:Name="parents" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Margin="0,0,8,0">
<Hyperlink
RequestNavigate="NavigateParent"
NavigateUri="{Binding .}"
ToolTip="NAVIGATE TO COMMIT">
<Run Text="{Binding .}"/>
</Hyperlink>
</Label>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- AUTHOR -->
<Label Grid.Row="2" Grid.Column="0" Content="AUTHOR" HorizontalAlignment="Right" Opacity=".6"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="author"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- AUTHOR TIME -->
<Label Grid.Row="2" Grid.Column="2" Content="AUTHOR TIME" HorizontalAlignment="Right" Opacity=".6"/>
<TextBox Grid.Row="2" Grid.Column="3" Grid.ColumnSpan="3" x:Name="authorTime"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="8,0,0,0"/>
<!-- COMMITTER -->
<Label Grid.Row="3" Grid.Column="0" Content="COMMITTER" HorizontalAlignment="Right" Opacity=".6"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="committer"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- COMMIT TIME -->
<Label Grid.Row="3" Grid.Column="2" Content="COMMIT TIME" HorizontalAlignment="Right" Opacity=".6"/>
<TextBox Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="3" x:Name="committerTime"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="8,0,0,0"/>
<Rectangle Grid.Row="4" Grid.ColumnSpan="4" Height="1" Margin="8,0" Fill="{StaticResource Brush.Border2}"/>
<!-- SUBJECT -->
<Label Grid.Row="5" Grid.Column="0" Content="SUBJECT" HorizontalAlignment="Right" Opacity=".6"/>
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="3"
x:Name="subject"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
Margin="8,0,16,0"/>
<!-- MESSAGE -->
<Label Grid.Row="6" Grid.Column="0" Content="DESCRIPTION" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity=".6"/>
<TextBox Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="3"
x:Name="message"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
VerticalAlignment="Center"
FontSize="11"
Margin="11,8,0,0"/>
<Rectangle Grid.Row="7" Grid.ColumnSpan="4" Height="1" Margin="8,0" Fill="{StaticResource Brush.Border2}"/>
<!-- CHANGELIST -->
<Label Grid.Row="8" Grid.Column="0" Content="CHANGED" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity=".6"/>
<DataGrid
Grid.Row="8"
Grid.Column="1"
Grid.ColumnSpan="3"
x:Name="changeList1"
RowHeight="20"
Margin="11,2,0,2">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
<EventSetter Event="MouseDoubleClick" Handler="ChangeListMouseDoubleClick"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
</TabItem>
<!-- CHANGES -->
<TabItem Header="CHANGES">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="2,0">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="BoolToCollapsed"/>
<converters:InverseBoolToCollapsed x:Key="InverseBoolToCollapsed"/>
</Grid.Resources>
<Grid Grid.Row="0" Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Grid.ColumnSpan="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" Background="{StaticResource Brush.BG3}"/>
<Path Grid.Column="0" Width="14" Height="14" Fill="{StaticResource Brush.FG2}" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<TextBox Grid.Column="1" x:Name="txtChangeFilter" BorderThickness="0" helpers:TextBoxHelper.Placeholder="Search File ..." TextChanged="SearchChangeFileTextChanged"/>
<ToggleButton
Grid.Column="2"
x:Name="toggleSwitchMode"
Margin="4,0,0,0"
ToolTip="SWITCH TO LIST/TREE VIEW"
Style="{StaticResource Style.ToggleButton.ListOrTree}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIUseListInChanges, Mode=TwoWay}"/>
</Grid>
<TreeView
Grid.Row="1"
x:Name="changeTree"
FontFamily="Consolas"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource InverseBoolToCollapsed}}"
Background="{StaticResource Brush.BG2}"
SelectedItemChanged="ChangeTreeItemSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Border x:Name="status" Width="14" Height="14" Visibility="Collapsed" Background="{Binding Change, Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="0,0,4,0">
<TextBlock Text="{Binding Change, Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="10" RenderOptions.BitmapScalingMode="HighQuality"/>
</Border>
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="4,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="status" Property="Visibility" Value="Visible"/>
<Setter TargetName="icon" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<DataGrid
Grid.Row="1"
x:Name="changeList2"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource BoolToCollapsed}}"
RowHeight="24"
SelectionChanged="ChangeListSelectionChanged"
SelectionMode="Single"
SelectionUnit="FullRow"
Background="{StaticResource Brush.BG2}">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<local:DiffViewer Grid.Column="2" x:Name="diffViewer" Background="{StaticResource Brush.BG3}"/>
</Grid>
</TabItem>
<!-- FILE TREE -->
<TabItem Header="FILES">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="2" Background="{StaticResource Brush.BG2}">
<TreeView x:Name="fileTree" SelectedItemChanged="FileTreeItemSelected" FontFamily="Consolas" PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="6,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.File}"/>
<Setter TargetName="icon" Property="Fill" Value="{StaticResource Brush.FG}"/>
<Setter TargetName="icon" Property="Opacity" Value=".75"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<Border Grid.Column="2" BorderThickness="1" Margin="2,0" BorderBrush="{StaticResource Brush.Border2}">
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<TextBlock
FontSize="10pt"
FontFamily="Consolas"
Padding="8"
Opacity="0.8"
Background="{StaticResource Brush.BG2}"
Foreground="{StaticResource Brush.FG}"
x:Name="filePreview"/>
</ScrollViewer>
<StackPanel x:Name="maskRevision" Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="Collapsed">
<Path x:Name="iconPreviewRevision" Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Submodule}" Fill="{StaticResource Brush.FG2}"/>
<Label x:Name="txtPreviewRevision" Margin="0,16,0,0" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
<StackPanel x:Name="maskPreviewNotSupported" Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="Collapsed">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Info}" Fill="{StaticResource Brush.FG2}"/>
<Label Margin="0,16,0,0" Content="BINARY FILE DETECTED" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</TabItem>
</TabControl>
</UserControl>

View file

@ -0,0 +1,504 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// Commit detail viewer
/// </summary>
public partial class CommitViewer : UserControl {
private Git.Repository repo = null;
private Git.Commit commit = null;
private List<Git.Change> cachedChanges = new List<Git.Change>();
private List<Git.Change> displayChanges = new List<Git.Change>();
private string changeFilter = null;
/// <summary>
/// Node for file tree.
/// </summary>
public class Node {
public string FilePath { get; set; } = "";
public string OriginalPath { get; set; } = "";
public string Name { get; set; } = "";
public bool IsFile { get; set; } = false;
public bool IsNodeExpanded { get; set; } = true;
public Git.Change Change { get; set; } = null;
public Git.Commit.Object CommitObject { get; set; } = null;
public List<Node> Children { get; set; } = new List<Node>();
}
/// <summary>
/// Constructor.
/// </summary>
public CommitViewer() {
InitializeComponent();
}
#region DATA
public void SetData(Git.Repository opened, Git.Commit selected) {
repo = opened;
commit = selected;
SetBaseInfo(commit);
Task.Run(() => {
cachedChanges.Clear();
cachedChanges = commit.GetChanges(repo);
Dispatcher.Invoke(() => {
changeList1.ItemsSource = null;
changeList1.ItemsSource = cachedChanges;
});
LayoutChanges();
SetRevisionFiles(commit.GetFiles(repo));
});
}
private void Cleanup(object sender, RoutedEventArgs e) {
fileTree.ItemsSource = null;
changeList1.ItemsSource = null;
changeList2.ItemsSource = null;
displayChanges.Clear();
cachedChanges.Clear();
diffViewer.Reset();
}
#endregion
#region BASE_INFO
private void SetBaseInfo(Git.Commit commit) {
var parentIds = new List<string>();
foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8));
SHA.Text = commit.SHA;
refs.ItemsSource = commit.Decorators;
parents.ItemsSource = parentIds;
author.Text = $"{commit.Author.Name} <{commit.Author.Email}>";
authorTime.Text = commit.Author.Time;
committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>";
committerTime.Text = commit.Committer.Time;
subject.Text = commit.Subject;
message.Text = commit.Message.Trim();
if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed;
else lblRefs.Visibility = Visibility.Visible;
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
committerRow.Height = new GridLength(0);
} else {
committerRow.Height = GridLength.Auto;
}
}
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
e.Handled = true;
}
#endregion
#region CHANGES
private void LayoutChanges() {
displayChanges.Clear();
if (string.IsNullOrEmpty(changeFilter)) {
displayChanges.AddRange(cachedChanges);
} else {
foreach (var c in cachedChanges) {
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
}
}
List<Node> changeTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
bool isDefaultExpanded = displayChanges.Count < 50;
foreach (var c in displayChanges) {
var sepIdx = c.Path.IndexOf('/');
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = c.Path;
node.IsFile = true;
node.Name = c.Path;
node.Change = c;
node.IsNodeExpanded = isDefaultExpanded;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
changeTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = c.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = isDefaultExpanded;
changeTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = isDefaultExpanded;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = c.Path;
node.Name = c.Path.Substring(start);
node.IsFile = true;
node.Change = c;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(changeTreeSource);
Dispatcher.Invoke(() => {
changeList2.ItemsSource = null;
changeList2.ItemsSource = displayChanges;
changeTree.ItemsSource = changeTreeSource;
diffViewer.Reset();
});
}
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
changeFilter = txtChangeFilter.Text.ToUpper();
Task.Run(() => LayoutChanges());
}
private void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
diffViewer.Reset();
var node = e.NewValue as Node;
if (node == null || !node.IsFile) return;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
diffViewer.Diff(repo, new DiffViewer.Option() {
RevisionRange = new string[] { start, commit.SHA },
Path = node.FilePath,
OrgPath = node.OriginalPath
});
}
private void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return;
var change = e.AddedItems[0] as Git.Change;
if (change == null) return;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
diffViewer.Diff(repo, new DiffViewer.Option() {
RevisionRange = new string[] { start, commit.SHA },
Path = change.Path,
OrgPath = change.OriginalPath
});
}
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Git.Change;
if (change == null) return;
var path = change.Path;
var menu = new ContextMenu();
if (change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, path);
viewer.Show();
};
menu.Items.Add(history);
MenuItem blame = new MenuItem();
blame.Header = "Blame";
blame.Click += (obj, ev) => {
Blame viewer = new Blame(repo, path, commit.SHA);
viewer.Show();
};
menu.Items.Add(blame);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
Process.Start("explorer", $"/select,{absPath}");
e.Handled = true;
};
menu.Items.Add(explore);
MenuItem saveAs = new MenuItem();
saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = change.Path;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path));
repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath);
}
};
menu.Items.Add(saveAs);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
private void ChangeListMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Git.Change;
if (change == null) return;
var viewer = new FileHistories(repo, change.Path);
viewer.Show();
}
#endregion
#region FILES
private void SetRevisionFiles(List<Git.Commit.Object> files) {
List<Node> fileTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
foreach (var obj in files) {
var sepIdx = obj.Path.IndexOf("/");
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = obj.Path;
node.Name = obj.Path;
node.IsFile = true;
node.IsNodeExpanded = false;
node.CommitObject = obj;
fileTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = obj.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = false;
fileTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = false;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = obj.Path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = obj.Path;
node.Name = obj.Path.Substring(start);
node.IsFile = true;
node.IsNodeExpanded = false;
node.CommitObject = obj;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(fileTreeSource);
Dispatcher.Invoke(() => {
fileTree.ItemsSource = fileTreeSource;
filePreview.Text = "";
});
}
private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
filePreview.Text = "";
maskPreviewNotSupported.Visibility = Visibility.Collapsed;
maskRevision.Visibility = Visibility.Collapsed;
var node = e.NewValue as Node;
if (node == null || !node.IsFile || node.CommitObject == null) return;
switch (node.CommitObject.Kind) {
case Git.Commit.Object.Type.Blob:
await Task.Run(() => {
var isBinary = false;
var data = commit.GetTextFileContent(repo, node.FilePath, out isBinary);
if (isBinary) {
Dispatcher.Invoke(() => maskPreviewNotSupported.Visibility = Visibility.Visible);
} else {
Dispatcher.Invoke(() => filePreview.Text = data);
}
});
break;
case Git.Commit.Object.Type.Tag:
maskRevision.Visibility = Visibility.Visible;
iconPreviewRevision.Data = FindResource("Icon.Tag") as Geometry;
txtPreviewRevision.Content = "TAG: " + node.CommitObject.SHA;
break;
case Git.Commit.Object.Type.Commit:
maskRevision.Visibility = Visibility.Visible;
iconPreviewRevision.Data = FindResource("Icon.Submodule") as Geometry;
txtPreviewRevision.Content = "SUBMODULE: " + node.CommitObject.SHA;
break;
default:
return;
}
}
#endregion
#region TREE_COMMON
private void SortTreeNodes(List<Node> list) {
list.Sort((l, r) => {
if (l.IsFile) {
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
} else {
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
}
});
foreach (var sub in list) {
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
}
}
private ScrollViewer GetScrollViewer(FrameworkElement owner) {
if (owner == null) return null;
if (owner is ScrollViewer) return owner as ScrollViewer;
int n = VisualTreeHelper.GetChildrenCount(owner);
for (int i = 0; i < n; i++) {
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
var deep = GetScrollViewer(child);
if (deep != null) return deep;
}
return null;
}
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
var scroll = GetScrollViewer(sender as TreeView);
if (scroll == null) return;
if (e.Delta > 0) {
scroll.LineUp();
} else {
scroll.LineDown();
}
e.Handled = true;
}
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as TreeViewItem;
if (item == null) return;
var node = item.DataContext as Node;
if (node == null || !node.IsFile) return;
item.IsSelected = true;
ContextMenu menu = new ContextMenu();
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, node.FilePath);
viewer.Show();
};
menu.Items.Add(history);
MenuItem blame = new MenuItem();
blame.Header = "Blame";
blame.Click += (obj, ev) => {
Blame viewer = new Blame(repo, node.FilePath, commit.SHA);
viewer.Show();
};
menu.Items.Add(blame);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
menu.Items.Add(explore);
MenuItem saveAs = new MenuItem();
saveAs.Header = "Save As ...";
saveAs.IsEnabled = node.CommitObject == null || node.CommitObject.Kind == Git.Commit.Object.Type.Blob;
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = node.FilePath;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
var path = Path.Combine(dialog.SelectedPath, node.Name);
repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path);
}
};
menu.Items.Add(saveAs);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(node.FilePath);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,71 @@
<UserControl x:Class="SourceGit.UI.Configure"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="500" Width="500" Height="314">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="8"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="18"/>
<RowDefinition Height="36"/>
<RowDefinition Height="8"/>
<RowDefinition Height="102"/>
<RowDefinition Height="18"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 仓库帐号 -->
<Label Grid.Row="0" Grid.ColumnSpan="2" Content="CREDENTIAL" FontSize="16" FontWeight="DemiBold" Opacity=".85"/>
<Label Grid.Row="2" Grid.Column="0" Content="User : " HorizontalAlignment="Right"/>
<TextBox
Grid.Row="2"
Grid.Column="1"
Height="24"
Text="{Binding ElementName=me, Path=UserName, Mode=TwoWay}"
helpers:TextBoxHelper.Placeholder="User name for this repository"/>
<Label Grid.Row="3" Grid.Column="0" Content="Email : " HorizontalAlignment="Right"/>
<TextBox
Grid.Row="3"
Grid.Column="1"
Height="24"
Text="{Binding ElementName=me, Path=UserEmail, Mode=TwoWay}"
helpers:TextBoxHelper.Placeholder="Email address"/>
<!-- 提交模板 -->
<Label Grid.Row="5" Grid.ColumnSpan="2" Content="COMMIT TEMPLATE" FontSize="16" FontWeight="DemiBold" Opacity=".85"/>
<Label Grid.Row="7" Grid.Column="0" Content="Template : " HorizontalAlignment="Right" VerticalAlignment="Top"/>
<TextBox
Grid.Row="7"
Grid.Column="1"
Height="100"
AcceptsReturn="True"
AcceptsTab="True"
Padding="2"
Text="{Binding ElementName=me, Path=CommitTemplate, Mode=TwoWay}"/>
<!-- 操作 -->
<Grid Grid.Row="9" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Save" Content="SAVE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Close" Content="CLOSE" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,71 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Repository configuration dialog
/// </summary>
public partial class Configure : UserControl {
private Git.Repository repo = null;
/// <summary>
/// User name for this repository.
/// </summary>
public string UserName { get; set; }
/// <summary>
/// User email for this repository.
/// </summary>
public string UserEmail { get; set; }
/// <summary>
/// Commit template for this repository.
/// </summary>
public string CommitTemplate { get; set; }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
public Configure(Git.Repository repo) {
this.repo = repo;
UserName = repo.GetConfig("user.name");
UserEmail = repo.GetConfig("user.email");
CommitTemplate = repo.CommitTemplate;
InitializeComponent();
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
var popup = App.GetPopupManager(repo);
popup?.Show(new Configure(repo));
}
#region EVENTS
private void Save(object sender, RoutedEventArgs e) {
var oldUser = repo.GetConfig("user.name");
if (oldUser != UserName) repo.SetConfig("user.name", UserName);
var oldEmail = repo.GetConfig("user.email");
if (oldEmail != UserEmail) repo.SetConfig("user.email", UserEmail);
if (CommitTemplate != repo.CommitTemplate) {
repo.CommitTemplate = CommitTemplate;
Git.Preference.Save();
}
Close(sender, e);
}
private void Close(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
#endregion
}
}

View file

@ -0,0 +1,80 @@
<UserControl x:Class="SourceGit.UI.CreateBranch"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="224" Width="500">
<UserControl.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Create Local Branch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Based On :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path x:Name="basedOnType" Width="12" Style="{StaticResource Style.Icon}"/>
<Label x:Name="basedOnDesc" VerticalAlignment="Center" Content="master"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="New Branch Name :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="txtName"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Enter branch name.">
<TextBox.Text>
<Binding ElementName="me" Path="BranchName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:BranchNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" Content="Local Changes :"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal">
<RadioButton Content="Stash &amp; Reapply" GroupName="LocalChanges" IsChecked="{Binding AutoStash, ElementName=me}"/>
<RadioButton Content="Discard" Margin="8,0,0,0" GroupName="LocalChanges" IsChecked="{Binding AutoStash, ElementName=me, Mode=OneWay, Converter={StaticResource InverseBool}}"/>
</StackPanel>
<CheckBox Grid.Row="5" Grid.Column="1"
x:Name="chkCheckout"
VerticalAlignment="Center"
IsChecked="True"
Content="Check out after created"/>
<Grid Grid.Row="7" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Create branch dialog
/// </summary>
public partial class CreateBranch : UserControl {
private Git.Repository repo = null;
private string based = null;
/// <summary>
/// New branch name.
/// </summary>
public string BranchName {
get;
set;
}
/// <summary>
/// Auto Stash
/// </summary>
public bool AutoStash { get; set; } = false;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
public CreateBranch(Git.Repository opened) {
InitializeComponent();
repo = opened;
nameValidator.Repo = opened;
}
/// <summary>
/// Create branch based on current head.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
var current = repo.CurrentBranch();
if (current != null) Show(repo, current);
}
/// <summary>
/// Create branch base on existed one.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
var dialog = new CreateBranch(repo);
dialog.based = branch.Name;
dialog.basedOnType.Data = dialog.FindResource("Icon.Branch") as Geometry;
dialog.basedOnDesc.Content = branch.Name;
if (!branch.IsLocal) dialog.txtName.Text = branch.Name.Substring(branch.Remote.Length + 1);
var popup = App.GetPopupManager(repo);
popup?.Show(dialog);
}
/// <summary>
/// Create branch based on tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="tag"></param>
public static void Show(Git.Repository repo, Git.Tag tag) {
var dialog = new CreateBranch(repo);
dialog.based = tag.Name;
dialog.basedOnType.Data = dialog.FindResource("Icon.Tag") as Geometry;
dialog.basedOnDesc.Content = tag.Name;
var popup = App.GetPopupManager(repo);
popup?.Show(dialog);
}
/// <summary>
/// Create branch based on commit.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var dialog = new CreateBranch(repo);
dialog.based = commit.SHA;
dialog.basedOnType.Data = dialog.FindResource("Icon.Commit") as Geometry;
dialog.basedOnDesc.Content = $"{commit.ShortSHA} {commit.Subject}";
var popup = App.GetPopupManager(repo);
popup?.Show(dialog);
}
/// <summary>
/// Start create branch.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtName)) return;
var popup = App.GetPopupManager(repo);
popup?.Lock();
bool checkout = chkCheckout.IsChecked == true;
await Task.Run(() => {
if (checkout) {
bool stashed = false;
if (repo.LocalChanges().Count > 0 && AutoStash) {
Git.Stash.Push(repo, true, "CREATE BRANCH AUTO STASH", new List<string>());
stashed = true;
}
repo.Checkout($"-b {BranchName} {based}");
if (stashed) {
var stashes = repo.Stashes();
if (stashes.Count > 0) stashes[0].Pop(repo);
}
} else {
Git.Branch.Create(repo, BranchName, based);
}
});
popup?.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

View file

@ -0,0 +1,70 @@
<UserControl x:Class="SourceGit.UI.CreateTag"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="224" d:DesignWidth="500" Width="500" Height="224">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="64"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Create Tag"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="New Tag At :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" x:Name="basedOnType" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}"/>
<Label x:Name="basedOnDesc" VerticalAlignment="Center" Content="xxx"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Tag Name :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="tagName"
Height="24"
helpers:TextBoxHelper.Placeholder="Recommanded format v1.0.0-alpha">
<TextBox.Text>
<Binding ElementName="me" Path="TagName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:TagNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,4" Content="Tag Message :"/>
<TextBox Grid.Row="4" Grid.Column="1"
x:Name="tagMessage"
Height="56"
Padding="2"
AcceptsReturn="True"
helpers:TextBoxHelper.Placeholder="Optional"
helpers:TextBoxHelper.PlaceholderBaseline="Top"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,98 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Create tag dialog
/// </summary>
public partial class CreateTag : UserControl {
private Git.Repository repo = null;
private string based = null;
/// <summary>
/// Tag name
/// </summary>
public string TagName { get; set; }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
public CreateTag(Git.Repository opened) {
InitializeComponent();
repo = opened;
nameValidator.Repo = opened;
}
/// <summary>
/// Create tag using current branch.
/// </summary>
/// <param name="repo">Opened repository.</param>
public static void Show(Git.Repository repo) {
Show(repo, repo.Branches().First(b => b.IsCurrent));
}
/// <summary>
/// Create tag using branch
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
if (branch == null) {
App.RaiseError("Empty repository!");
return;
}
var dialog = new CreateTag(repo);
dialog.based = branch.Head;
dialog.basedOnType.Data = dialog.FindResource("Icon.Branch") as Geometry;
dialog.basedOnDesc.Content = branch.Name;
var popup = App.GetPopupManager(repo);
popup?.Show(dialog);
}
/// <summary>
/// Create tag using commit.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var dialog = new CreateTag(repo);
dialog.based = commit.SHA;
dialog.basedOnType.Data = dialog.FindResource("Icon.Commit") as Geometry;
dialog.basedOnDesc.Content = $"{commit.ShortSHA} {commit.Subject}";
var popup = App.GetPopupManager(repo);
popup?.Show(dialog);
}
/// <summary>
/// Start to create tag.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
tagName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(tagName)) return;
Git.Tag.Add(repo, TagName, based, tagMessage.Text);
var popup = App.GetPopupManager(repo);
popup?.Close();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

526
SourceGit/UI/Dashboard.xaml Normal file
View file

@ -0,0 +1,526 @@
<UserControl x:Class="SourceGit.UI.Dashboard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:source="clr-namespace:SourceGit"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<UserControl.Resources>
<RoutedUICommand x:Key="OpenSearchBarCommand" Text="OpenSearchBar"/>
<RoutedUICommand x:Key="HideSearchBarCommand" Text="HideSearchBar"/>
</UserControl.Resources>
<UserControl.InputBindings>
<KeyBinding Key="F" Modifiers="Ctrl" Command="{StaticResource OpenSearchBarCommand}"/>
<KeyBinding Key="ESC" Command="{StaticResource HideSearchBarCommand}"/>
</UserControl.InputBindings>
<UserControl.CommandBindings>
<CommandBinding Command="{StaticResource OpenSearchBarCommand}" Executed="OpenSearchBar"/>
<CommandBinding Command="{StaticResource HideSearchBarCommand}" Executed="HideSearchBar"/>
</UserControl.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- TitleBar -->
<Grid Grid.Row="0" Panel.ZIndex="9999">
<Border Background="{StaticResource Brush.BG1}">
<Border.Effect>
<DropShadowEffect ShadowDepth="2" Direction="270" Opacity=".5" Color="Black"/>
</Border.Effect>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Browser -->
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="6,0">
<Button Click="OpenExplorer" Margin="4,0,0,0" ToolTip="Open In File Browser">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder.Open}"/>
<Label Content="Explore"/>
</StackPanel>
</Button>
<Button Click="OpenConfigure" Margin="4,0,0,0" ToolTip="Configure This Repository">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Setting}"/>
<Label Content="Configure"/>
</StackPanel>
</Button>
</StackPanel>
<!-- Common Git Options -->
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="6,0">
<Button Click="OpenFetch" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Fetch}"/>
<Label Content="Fetch"/>
</StackPanel>
</Button>
<Button Click="OpenPull" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Pull}"/>
<Label Content="Pull"/>
</StackPanel>
</Button>
<Button Click="OpenPush" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Push}"/>
<Label Content="Push"/>
</StackPanel>
</Button>
<Button Click="OpenStash" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.SaveStash}"/>
<Label Content="Stash"/>
</StackPanel>
</Button>
<Button Click="OpenApply">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Apply}"/>
<Label Content="Apply"/>
</StackPanel>
</Button>
</StackPanel>
<!-- External Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Click="OpenSearch" Margin="4,0" ToolTip="Search Commit">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<Label Content="Search"/>
</StackPanel>
</Button>
<Button Click="OpenTerminal" Margin="4,0" ToolTip="Open Git Bash">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Terminal}"/>
<Label Content="Terminal"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
<!-- Main body -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left panel -->
<Grid Grid.Column="0" x:Name="main" Background="{StaticResource Brush.BG4}">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
<RowDefinition Height="24"/>
<RowDefinition Height="1"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="Bool2Collapsed"/>
</Grid.Resources>
<!-- WORKSPACE -->
<Label Grid.Row="0" Margin="4,0,0,0" Content="WORKSPACE" Style="{StaticResource Style.Label.GroupHeader}" />
<ListView
Grid.Row="1"
x:Name="workspace"
Background="{StaticResource Brush.BG3}"
Style="{StaticResource Style.ListView.Borderless}"
SelectionMode="Single">
<ListViewItem x:Name="historiesSwitch" Selected="SwitchHistories" IsSelected="True">
<StackPanel Margin="16,0,0,0" Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Histories}"/>
<Label Margin="4,0,0,0" Content="Histories"/>
</StackPanel>
</ListViewItem>
<ListViewItem x:Name="workingCopySwitch" Selected="SwitchWorkingCopy">
<Grid Margin="16,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.WorkingCopy}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="Commit"/>
<Border Grid.Column="2" x:Name="localChangesBadge" Style="{StaticResource Style.Border.Badge}">
<Label x:Name="localChangesCount" Margin="4,-2,4,-2" Content="999" FontSize="10"/>
</Border>
</Grid>
</ListViewItem>
<ListViewItem x:Name="stashesSwitch" Selected="SwitchStashes">
<Grid Margin="16,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Stashes}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="Stashes"/>
<Border Grid.Column="2" x:Name="stashBadge" Style="{StaticResource Style.Border.Badge}">
<Label x:Name="stashCount" Margin="4,-2,4,-2" Content="999" FontSize="10"/>
</Border>
</Grid>
</ListViewItem>
</ListView>
<!-- LOCAL BRANCHES -->
<Grid Grid.Row="2" Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="LOCAL BRANCHES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenGitFlow" Background="Transparent" ToolTip="GIT FLOW">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Flow}"/>
</Button>
<Button Grid.Column="3" Click="OpenNewBranch" Background="Transparent" ToolTip="NEW BRANCH">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Branch.Add}"/>
</Button>
</Grid>
<TreeView
Grid.Row="3"
x:Name="localBranchTree"
Background="{StaticResource Brush.BG3}"
FontFamily="Consolas"
LostFocus="TreeLostFocus"
SelectedItemChanged="LocalBranchSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="LocalBranchContextMenuOpening"/>
<EventSetter Event="MouseDoubleClick" Handler="LocalBranchMouseDoubleClick"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<Label Grid.Column="1" x:Name="name" Content="{Binding Name}" Padding="4,0,0,0"/>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<Border Style="{StaticResource Style.Border.Badge}" Visibility="{Binding TrackVisibility}">
<Label Margin="4,-2,4,-2" Content="{Binding Branch.UpstreamTrack}" FontSize="10"/>
</Border>
<ToggleButton
Visibility="{Binding FilterVisibility}"
IsChecked="{Binding IsFiltered, Mode=OneWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</StackPanel>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter TargetName="name" Property="FontWeight" Value="ExtraBold"/>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Check}"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- REMOTES -->
<Grid Grid.Row="4" Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="REMOTES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenRemote" ToolTip="ADD REMOTE">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Remote.Add}"/>
</Button>
</Grid>
<TreeView
Grid.Row="5"
x:Name="remoteBranchTree"
Background="{StaticResource Brush.BG3}"
FontFamily="Consolas"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectedItemChanged="RemoteBranchSelected"
LostFocus="TreeLostFocus"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="RemoteContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:RemoteNode}" ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}"/>
<TextBlock Grid.Column="1" x:Name="name" Text="{Binding Name}" Padding="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}" ClipToBounds="True"/>
</Grid>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:BranchNode}" ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<TextBlock Grid.Column="1" x:Name="name" Text="{Binding Name}" Padding="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}" ClipToBounds="True"/>
<ToggleButton
Grid.Column="2"
Visibility="{Binding FilterVisibility}"
IsChecked="{Binding IsFiltered, Mode=OneWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
<!-- TAGS -->
<ToggleButton
x:Name="tagListToggle"
Grid.Row="6"
Style="{StaticResource Style.ToggleButton.Expender}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIShowTags, Mode=TwoWay}">
<Grid Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="tagCount" Content="TAGS" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenNewTag" ToolTip="NEW TAG">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag.Add}"/>
</Button>
</Grid>
</ToggleButton>
<Rectangle Grid.Row="7" Height="1" Fill="{StaticResource Brush.BG3}"/>
<DataGrid
Grid.Row="8"
x:Name="tagList"
Visibility="{Binding ElementName=tagListToggle, Path=IsChecked, Converter={StaticResource Bool2Collapsed}}"
Background="{StaticResource Brush.BG3}"
RowHeight="24"
Height="200"
LostFocus="TagLostFocus"
SelectionChanged="TagSelectionChanged"
ContextMenuOpening="TagContextMenuOpening"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Resources>
<Style x:Key="Style.DataGridText.TagName" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="26">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" IsReadOnly="True" Binding="{Binding Name}" ElementStyle="{StaticResource Style.DataGridText.TagName}"/>
<DataGridTemplateColumn Width="16">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ToggleButton
Grid.Column="2"
IsChecked="{Binding IsFiltered, Mode=TwoWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- SUBMODULES -->
<ToggleButton
x:Name="submoduleListToggle"
Grid.Row="9"
Style="{StaticResource Style.ToggleButton.Expender}"
IsChecked="False">
<Grid Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="submoduleCount" Content="SUBMODULES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenAddSubmodule" ToolTip="ADD SUBMODULE">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Submodule}"/>
</Button>
<Button Grid.Column="3" Click="UpdateSubmodule" ToolTip="UPDATE SUBMODULE">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}"/>
</Button>
</Grid>
</ToggleButton>
<DataGrid
Grid.Row="11"
x:Name="submoduleList"
Visibility="{Binding ElementName=submoduleListToggle, Path=IsChecked, Converter={StaticResource Bool2Collapsed}}"
Background="{StaticResource Brush.BG3}"
RowHeight="24"
Height="100"
LostFocus="SubmoduleLostFocus"
ContextMenuOpening="SubmoduleContextMenuOpening"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Resources>
<Style x:Key="Style.DataGridText.SubmodulePath" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
</Style>
</DataGrid.Resources>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="MouseDoubleClick" Handler="SubmoduleMouseDoubleClick"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Width="26">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Submodule}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" IsReadOnly="True" Binding="{Binding}" ElementStyle="{StaticResource Style.DataGridText.SubmodulePath}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Splitter -->
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.BG3}"/>
<!-- Right -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Abort panel -->
<Grid x:Name="abortPanel" Grid.Row="0" Background="LightGoldenrodYellow" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="txtMergeProcessing" FontWeight="DemiBold" Foreground="{StaticResource Brush.BG4}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="btnResolve" Click="Resolve" Content="RESOLVE" Margin="4">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.Bordered}">
<Setter Property="Background" Value="{StaticResource Brush.BG1}"/>
<Setter Property="Margin" Value="2"/>
</Style>
</Button.Style>
</Button>
<Button x:Name="btnContinue" Click="Continue" Content="CONTINUE" Style="{StaticResource Style.Button.AccentBordered}" Margin="4"/>
<Button Grid.Column="3" Click="Abort" Content="ABORT" Style="{StaticResource Style.Button.Bordered}" Foreground="{StaticResource Brush.BG1}" Margin="4"/>
</StackPanel>
</Grid>
<!-- Others -->
<local:Histories Grid.Row="1" x:Name="histories" Visibility="Visible"/>
<local:WorkingCopy Grid.Row="1" x:Name="commits" Visibility="Collapsed"/>
<local:Stashes Grid.Row="1" x:Name="stashes" Visibility="Collapsed"/>
</Grid>
</Grid>
<!-- Popups -->
<local:PopupManager x:Name="popupManager" Grid.Row="1"/>
</Grid>
</UserControl>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
<UserControl x:Class="SourceGit.UI.DeleteBranch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Branch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Branch :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}" Margin="4,0"/>
<Label x:Name="branchName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,56 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Confirm to delete branch
/// </summary>
public partial class DeleteBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch branch = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository.</param>
/// <param name="target">Branch to be deleted.</param>
public DeleteBranch(Git.Repository opened, Git.Branch target) {
InitializeComponent();
repo = opened;
branch = target;
branchName.Content = target.Name;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="opened"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository opened, Git.Branch branch) {
var popup = App.GetPopupManager(opened);
popup?.Show(new DeleteBranch(opened, branch));
}
/// <summary>
/// Delete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
var popup = App.GetPopupManager(repo);
popup?.Lock();
await Task.Run(() => branch.Delete(repo));
popup?.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

View file

@ -0,0 +1,42 @@
<UserControl x:Class="SourceGit.UI.DeleteRemote"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Remote"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Remote :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}" Margin="4,0"/>
<Label x:Name="remoteName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,57 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Confirm to delete a remote
/// </summary>
public partial class DeleteRemote : UserControl {
private Git.Repository repo = null;
private string remote = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
/// <param name="target">Remote to be deleted</param>
public DeleteRemote(Git.Repository opened, string target) {
InitializeComponent();
repo = opened;
remote = target;
remoteName.Content = target;
}
/// <summary>
/// Show this dialog
/// </summary>
/// <param name="opened"></param>
/// <param name="remote"></param>
public static void Show(Git.Repository opened, string remote) {
var popup = App.GetPopupManager(opened);
popup?.Show(new DeleteRemote(opened, remote));
}
/// <summary>
/// Delete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
var popup = App.GetPopupManager(repo);
popup?.Lock();
await Task.Run(() => Git.Remote.Delete(repo, remote));
popup?.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

View file

@ -0,0 +1,45 @@
<UserControl x:Class="SourceGit.UI.DeleteTag"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Tag"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Tag :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag}" Margin="4,0"/>
<Label x:Name="tagName"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="chkWithRemote" Content="Delete from remote repositories"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,61 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Delete tag dialog.
/// </summary>
public partial class DeleteTag : UserControl {
private Git.Repository repo = null;
private Git.Tag tag = null;
/// <summary>
/// Constructor
/// </summary>
/// <param name="repo">Opened repo</param>
/// <param name="tag">Delete tag</param>
public DeleteTag(Git.Repository repo, Git.Tag tag) {
this.repo = repo;
this.tag = tag;
InitializeComponent();
tagName.Content = tag.Name;
}
/// <summary>
/// Display this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="tag"></param>
public static void Show(Git.Repository repo, Git.Tag tag) {
var popup = App.GetPopupManager(repo);
popup?.Show(new DeleteTag(repo, tag));
}
/// <summary>
/// Start request.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
var popup = App.GetPopupManager(repo);
popup?.Lock();
var push = chkWithRemote.IsChecked == true;
await Task.Run(() => Git.Tag.Delete(repo, tag.Name, push));
popup?.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

View file

@ -0,0 +1,203 @@
<UserControl x:Class="SourceGit.UI.DiffViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
FontFamily="Consolas">
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
<Grid Margin="8,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" x:Name="orgFileNamePanel" Orientation="Horizontal">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="orgFileName" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
<TextBlock Margin="8,0" VerticalAlignment="Center" Text="→" Foreground="{StaticResource Brush.FG}"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
<Path x:Name="loading" Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}"/>
</StackPanel>
<StackPanel Grid.Column="2" x:Name="diffNavigation" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="26" Click="Go2Next" ToolTip="Next Difference" Background="Transparent">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.MoveDown}"/>
</Button>
<Button Click="Go2Prev" ToolTip="Previous Difference" Background="Transparent">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.MoveUp}"/>
</Button>
</StackPanel>
</Grid>
</Border>
<Grid x:Name="textChange" Grid.Row="1" ClipToBounds="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="100"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="leftLineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
BorderThickness="0"
Background="Transparent"
IsReadOnly="True"
Padding="2,0"
Margin="0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="leftText"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="LeftSizeChanged"
SelectionChanged="OnViewerSelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy"/>
</ContextMenu>
</RichTextBox.ContextMenu>
</RichTextBox>
</Grid>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="rightLineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Padding="2,0"
Margin="0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="rightText"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="RightSizeChanged"
SelectionChanged="OnViewerSelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy"/>
</ContextMenu>
</RichTextBox.ContextMenu>
</RichTextBox>
</Grid>
</Grid>
<Border x:Name="sizeChange" Grid.Row="1" ClipToBounds="True" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Label Content="BINARY DIFF" Margin="0,0,0,32" FontSize="18" FontWeight="UltraBold" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Center"/>
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Binary}" Fill="{StaticResource Brush.FG2}"/>
<Grid Margin="0,16,0,0" HorizontalAlignment="Center" TextElement.FontSize="18" TextElement.FontWeight="UltraBold">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="64"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="OLD :" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="txtOldSize" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Right"/>
<Label Grid.Row="1" Grid.Column="0" Content="NEW :" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="1" Grid.Column="1" x:Name="txtNewSize" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Right"/>
</Grid>
</StackPanel>
</Border>
<Border x:Name="noChange" Grid.Row="1" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Check}"/>
<Label Margin="0,8,0,0" Content="NO CHANGES OR ONLY EOL CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Diff}"/>
<Label Margin="0,8,0,0" Content="SELECT FILE TO VIEW CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
</UserControl>

View file

@ -0,0 +1,405 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Viewer for git diff
/// </summary>
public partial class DiffViewer : UserControl {
private double minWidth = 0;
/// <summary>
/// Diff options.
/// </summary>
public class Option {
public string[] RevisionRange = new string[] { };
public string Path = "";
public string OrgPath = null;
public string ExtraArgs = "";
}
/// <summary>
/// Constructor
/// </summary>
public DiffViewer() {
InitializeComponent();
Reset();
}
/// <summary>
/// Reset data.
/// </summary>
public void Reset() {
mask.Visibility = Visibility.Visible;
}
/// <summary>
/// Diff with options.
/// </summary>
/// <param name="repo"></param>
/// <param name="opts"></param>
public void Diff(Git.Repository repo, Option opts) {
SetTitle(opts.Path, opts.OrgPath);
loading.Visibility = Visibility.Visible;
mask.Visibility = Visibility.Collapsed;
textChange.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Collapsed;
Task.Run(() => {
var args = $"{opts.ExtraArgs} ";
if (opts.RevisionRange.Length > 0) args += $"{opts.RevisionRange[0]} ";
if (opts.RevisionRange.Length > 1) args += $"{opts.RevisionRange[1]} -- ";
if (!string.IsNullOrEmpty(opts.OrgPath)) args += $"\"{opts.OrgPath}\" ";
args += $"\"{opts.Path}\"";
var rs = Git.Diff.Run(repo, args);
if (rs.IsBinary) {
SetSizeChangeData(Git.Diff.GetSizeChange(repo, opts.RevisionRange, opts.Path, opts.OrgPath));
} else if (rs.Blocks.Count > 0) {
SetData(rs);
} else {
SetSame();
}
});
}
#region LAYOUT
/// <summary>
/// Show diff title
/// </summary>
/// <param name="file"></param>
/// <param name="orgFile"></param>
private void SetTitle(string file, string orgFile) {
fileName.Text = file;
if (!string.IsNullOrEmpty(orgFile) && orgFile != "/dev/null") {
orgFileNamePanel.Visibility = Visibility.Visible;
orgFileName.Text = orgFile;
} else {
orgFileNamePanel.Visibility = Visibility.Collapsed;
}
}
/// <summary>
/// Show size changes.
/// </summary>
/// <param name="bc"></param>
private void SetSizeChangeData(Git.Diff.BinaryChange bc) {
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Visible;
diffNavigation.Visibility = Visibility.Collapsed;
txtNewSize.Content = $"{bc.Size} Bytes";
txtOldSize.Content = $"{bc.PreSize} Bytes";
});
}
/// <summary>
/// Show no changes or only EOL changes.
/// </summary>
private void SetSame() {
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Visible;
diffNavigation.Visibility = Visibility.Collapsed;
});
}
/// <summary>
/// Show diff content.
/// </summary>
/// <param name="rs"></param>
private void SetData(Git.Diff.Result rs) {
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
textChange.Visibility = Visibility.Visible;
diffNavigation.Visibility = Visibility.Visible;
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16;
leftLineNumber.Text = "";
rightLineNumber.Text = "";
leftText.Document.Blocks.Clear();
rightText.Document.Blocks.Clear();
foreach (var b in rs.Blocks) ShowBlock(b);
leftText.Document.PageWidth = minWidth + 16;
rightText.Document.PageWidth = minWidth + 16;
leftText.ScrollToHome();
});
}
/// <summary>
/// Make paragraph.
/// </summary>
/// <param name="b"></param>
private void ShowBlock(Git.Diff.Block b) {
var content = b.Builder.ToString();
Paragraph p = new Paragraph(new Run(content));
p.Margin = new Thickness(0);
p.Padding = new Thickness();
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
p.DataContext = b;
switch (b.Mode) {
case Git.Diff.LineMode.Normal:
break;
case Git.Diff.LineMode.Indicator:
p.Foreground = Brushes.Gray;
p.FontStyle = FontStyles.Italic;
break;
case Git.Diff.LineMode.Empty:
p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
break;
case Git.Diff.LineMode.Added:
p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
break;
case Git.Diff.LineMode.Deleted:
p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
break;
}
var formatter = new FormattedText(
content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
leftText.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) minWidth = formatter.Width;
switch (b.Side) {
case Git.Diff.Side.Left:
leftText.Document.Blocks.Add(p);
for (int i = 0; i < b.Count; i++) {
if (b.CanShowNumber) leftLineNumber.AppendText($"{i + b.LeftStart}\n");
else leftLineNumber.AppendText("\n");
}
break;
case Git.Diff.Side.Right:
rightText.Document.Blocks.Add(p);
for (int i = 0; i < b.Count; i++) {
if (b.CanShowNumber) rightLineNumber.AppendText($"{i + b.RightStart}\n");
else rightLineNumber.AppendText("\n");
}
break;
default:
leftText.Document.Blocks.Add(p);
var cp = new Paragraph(new Run(content));
cp.Margin = new Thickness(0);
cp.Padding = new Thickness();
cp.LineHeight = 1;
cp.Background = p.Background;
cp.Foreground = p.Foreground;
cp.FontStyle = p.FontStyle;
cp.DataContext = b;
rightText.Document.Blocks.Add(cp);
for (int i = 0; i < b.Count; i++) {
if (b.Mode != Git.Diff.LineMode.Indicator) {
leftLineNumber.AppendText($"{i + b.LeftStart}\n");
rightLineNumber.AppendText($"{i + b.RightStart}\n");
} else {
leftLineNumber.AppendText("\n");
rightLineNumber.AppendText("\n");
}
}
break;
}
}
#endregion
#region EVENTS
/// <summary>
/// Sync scroll both sides.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerScroll(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
if (leftText.VerticalOffset != e.VerticalOffset) {
leftText.ScrollToVerticalOffset(e.VerticalOffset);
}
if (rightText.VerticalOffset != e.VerticalOffset) {
rightText.ScrollToVerticalOffset(e.VerticalOffset);
}
leftLineNumber.Margin = new Thickness(0, -e.VerticalOffset, 0, 0);
rightLineNumber.Margin = new Thickness(0, -e.VerticalOffset, 0, 0);
} else {
if (leftText.HorizontalOffset != e.HorizontalOffset) {
leftText.ScrollToHorizontalOffset(e.HorizontalOffset);
}
if (rightText.HorizontalOffset != e.HorizontalOffset) {
rightText.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
}
/// <summary>
/// Scroll using mouse wheel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) {
var text = sender as RichTextBox;
if (text == null) return;
if (e.Delta > 0) {
text.LineUp();
} else {
text.LineDown();
}
e.Handled = true;
}
/// <summary>
/// Fix document size for left side.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LeftSizeChanged(object sender, SizeChangedEventArgs e) {
if (leftText.Document.PageWidth < leftText.ActualWidth) {
leftText.Document.PageWidth = leftText.ActualWidth;
}
}
/// <summary>
/// Fix document size for right side.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void RightSizeChanged(object sender, SizeChangedEventArgs e) {
if (rightText.Document.PageWidth < rightText.ActualWidth) {
rightText.Document.PageWidth = rightText.ActualWidth;
}
}
/// <summary>
/// Auto scroll when selection changed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerSelectionChanged(object sender, RoutedEventArgs e) {
var doc = sender as RichTextBox;
if (doc == null || doc.IsFocused == false) return;
if (Mouse.LeftButton == MouseButtonState.Pressed && !doc.Selection.IsEmpty) {
var p = Mouse.GetPosition(doc);
if (p.X <= 8) {
doc.LineLeft();
} else if (p.X >= doc.ActualWidth - 8) {
doc.LineRight();
}
if (p.Y <= 8) {
doc.LineUp();
} else if (p.Y >= doc.ActualHeight - 8) {
doc.LineDown();
}
}
}
/// <summary>
/// Go to next difference.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Go2Next(object sender, RoutedEventArgs e) {
Paragraph next = null;
double minTop = 0;
foreach (var p in leftText.Document.Blocks) {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top > 17 && block.IsLeftDelete) {
next = p as Paragraph;
minTop = rect.Top;
break;
}
}
foreach (var p in rightText.Document.Blocks) {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top > 17 && block.IsRightAdded) {
if (next == null || minTop > rect.Top) {
next = p as Paragraph;
minTop = rect.Top;
}
break;
}
}
if (next != null) {
rightText.ScrollToVerticalOffset(rightText.VerticalOffset + minTop - 16);
}
}
/// <summary>
/// Go to previous difference.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Go2Prev(object sender, RoutedEventArgs e) {
Paragraph next = null;
double maxTop = 0;
var p = leftText.Document.Blocks.LastBlock as Paragraph;
do {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top < 15 && block.IsLeftDelete) {
next = p;
maxTop = rect.Top;
break;
}
p = p.PreviousBlock as Paragraph;
} while (p != null);
p = rightText.Document.Blocks.LastBlock as Paragraph;
do {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top < 15 && block.IsRightAdded) {
if (next == null || maxTop < rect.Top) {
next = p;
maxTop = rect.Top;
}
break;
}
p = p.PreviousBlock as Paragraph;
} while (p != null);
if (next != null) {
rightText.ScrollToVerticalOffset(rightText.VerticalOffset + maxTop - 16);
}
}
#endregion
}
}

47
SourceGit/UI/Discard.xaml Normal file
View file

@ -0,0 +1,47 @@
<UserControl x:Class="SourceGit.UI.Discard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Discard Changes"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Changes :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path x:Name="icon" Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}" Fill="{StaticResource Brush.FG2}" Margin="4,0"/>
<Label x:Name="txtPath"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="1" Content="You can't undo this action!!!" Foreground="{StaticResource Brush.FG2}"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Confirm to discard changes dialog.
/// </summary>
public partial class Discard : UserControl {
private Git.Repository repo = null;
private List<Git.Change> changes = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="targets"></param>
public Discard(Git.Repository opened, List<Git.Change> targets) {
repo = opened;
changes = targets;
InitializeComponent();
if (changes == null || changes.Count == 0) {
txtPath.Content = "All local changes in working copy.";
icon.Data = FindResource("Icon.Folder") as Geometry;
} else if (changes.Count == 1) {
txtPath.Content = changes[0].Path;
} else {
txtPath.Content = $"Total {changes.Count} changes ...";
}
}
/// <summary>
/// Show this dialog
/// </summary>
/// <param name="opened"></param>
/// <param name="targets"></param>
public static void Show(Git.Repository opened, List<Git.Change> targets) {
var popup = App.GetPopupManager(opened);
popup?.Show(new Discard(opened, targets));
}
private async void Sure(object sender, RoutedEventArgs e) {
var popup = App.GetPopupManager(repo);
popup?.Lock();
await Task.Run(() => repo.Discard(changes));
popup?.Close(true);
}
private void Cancel(object sender, RoutedEventArgs e) {
App.GetPopupManager(repo)?.Close();
}
}
}

66
SourceGit/UI/Fetch.xaml Normal file
View file

@ -0,0 +1,66 @@
<UserControl x:Class="SourceGit.UI.Fetch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="192" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</Grid.Resources>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Fetch Remote Changes"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Remote :"/>
<ComboBox x:Name="combRemotes" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" IsEnabled="{Binding ElementName=chkFetchAll, Path=IsChecked, Converter={StaticResource InverseBool}}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type git:Remote}">
<StackPanel Orientation="Horizontal" Height="20">
<Path Margin="4,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}"/>
<Label Content="{Binding Name}" Padding="8,0,0,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Grid.Row="3" Grid.Column="1"
x:Name="chkFetchAll"
IsChecked="True"
Content="Fetch all remotes"/>
<CheckBox Grid.Row="4" Grid.Column="1"
x:Name="chkPrune"
IsChecked="True"
Content="Prune remote dead branches"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

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