Merge branch 'master' into feature/resx

This commit is contained in:
Enner Pérez 2024-03-17 21:11:24 -05:00 committed by GitHub
commit 14550655f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
235 changed files with 8625 additions and 4381 deletions

364
.editorconfig Normal file
View file

@ -0,0 +1,364 @@
root = true
# All files
[*]
indent_style = space
# Xml files
[*.xml]
indent_size = 2
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
[*.{cs,vb}]
# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:warning
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
[*.cs]
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# 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
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
[*.{cs,vb}]
# Naming rules
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
# Symbol specifications
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case

View file

@ -26,6 +26,8 @@ Opensouce Git GUI client.
* Revision Diffs * Revision Diffs
* GitFlow support * GitFlow support
> **Linux** only tested on **Ubuntu 22.04** on **X11**.
## How to use ## How to use
**To use this tool, you need to install Git first.** **To use this tool, you need to install Git first.**
@ -43,7 +45,7 @@ For **macOS** users:
For **Linux** users: For **Linux** users:
* `xdg-open` must be installed to support open native file manager. * `xdg-open` must be installed to support open native file manager.
* Only tested on `Ubuntu 22.04`. * Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
## Screen Shots ## Screen Shots
@ -55,12 +57,10 @@ For **Linux** users:
![Theme Light](./screenshots/theme_light.png) ![Theme Light](./screenshots/theme_light.png)
## Thanks ## Contributing
* [gigi81](https://github.com/gigi81) Github actions integration Thanks to all the people who contribute.
* [kekekeks](https://github.com/kekekeks) Way to stage/unstage/discard selected changes in a file.
* [XiaoLinger](https://gitee.com/LingerNN) Hotkey: `CTRL + Enter` to commit <a href="https://github.com/sourcegit-scm/sourcegit/graphs/contributors">
* [carterl](https://gitee.com/carterl) Supports Windows Terminal; Rewrite way to find git executable <img src="https://contrib.rocks/image?repo=sourcegit-scm/sourcegit" />
* [PUMA](https://gitee.com/whgfu) Configure for default user </a>
* [Rwing](https://gitee.com/rwing) GitFlow: add an option to keep branch after finish
* [XiaoLinger](https://gitee.com/LingerNN) Fix localizations in popup panel

View file

@ -1,3 +1,12 @@
using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
@ -6,22 +15,21 @@ using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Styling; using Avalonia.Styling;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
namespace SourceGit { namespace SourceGit
public partial class App : Application { {
public partial class App : Application
{
[STAThread] [STAThread]
public static void Main(string[] args) { public static void Main(string[] args)
try { {
try
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
} catch (Exception ex) { }
catch (Exception ex)
{
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("Crash: "); builder.Append("Crash: ");
builder.Append(ex.Message); builder.Append(ex.Message);
@ -42,37 +50,45 @@ namespace SourceGit {
} }
} }
public static AppBuilder BuildAvaloniaApp() { public static AppBuilder BuildAvaloniaApp()
{
var builder = AppBuilder.Configure<App>(); var builder = AppBuilder.Configure<App>();
builder.UsePlatformDetect(); builder.UsePlatformDetect();
builder.LogToTrace(); builder.LogToTrace();
builder.ConfigureFonts(manager => { builder.ConfigureFonts(manager =>
{
var monospace = new EmbeddedFontCollection( var monospace = new EmbeddedFontCollection(
new Uri("fonts:SourceGit", UriKind.Absolute), new Uri("fonts:SourceGit", UriKind.Absolute),
new Uri("avares://SourceGit/Resources/Fonts", UriKind.Absolute)); new Uri("avares://SourceGit/Resources/Fonts", UriKind.Absolute));
manager.AddFontCollection(monospace); manager.AddFontCollection(monospace);
}); });
Native.OS.SetupFonts(builder); Native.OS.SetupApp(builder);
return builder; return builder;
} }
public static void RaiseException(string context, string message) { public static void RaiseException(string context, string message)
if (Current is App app && app._notificationReceiver != null) { {
if (Current is App app && app._notificationReceiver != null)
{
var notice = new Models.Notification() { IsError = true, Message = message }; var notice = new Models.Notification() { IsError = true, Message = message };
app._notificationReceiver.OnReceiveNotification(context, notice); app._notificationReceiver.OnReceiveNotification(context, notice);
} }
} }
public static void SendNotification(string context, string message) { public static void SendNotification(string context, string message)
if (Current is App app && app._notificationReceiver != null) { {
if (Current is App app && app._notificationReceiver != null)
{
var notice = new Models.Notification() { IsError = false, Message = message }; var notice = new Models.Notification() { IsError = false, Message = message };
app._notificationReceiver.OnReceiveNotification(context, notice); app._notificationReceiver.OnReceiveNotification(context, notice);
} }
} }
public static void SetLocale(string localeKey) { public static void SetLocale(string localeKey)
{
var app = Current as App; var app = Current as App;
localeKey = localeKey.Replace("_", "-"); localeKey = localeKey.Replace("_", "-");
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(localeKey); Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(localeKey);
@ -92,7 +108,8 @@ namespace SourceGit {
return; return;
} }
if (app._activeLocale != null) { if (app._activeLocale != null)
{
app.Resources.MergedDictionaries.Remove(app._activeLocale); app.Resources.MergedDictionaries.Remove(app._activeLocale);
} }
@ -100,31 +117,42 @@ namespace SourceGit {
app._activeLocale = targetLocale; app._activeLocale = targetLocale;
} }
public static void SetTheme(string theme) { public static void SetTheme(string theme)
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) { {
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
{
Current.RequestedThemeVariant = ThemeVariant.Light; Current.RequestedThemeVariant = ThemeVariant.Light;
} else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) { }
else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase))
{
Current.RequestedThemeVariant = ThemeVariant.Dark; Current.RequestedThemeVariant = ThemeVariant.Dark;
} else { }
else
{
Current.RequestedThemeVariant = ThemeVariant.Default; Current.RequestedThemeVariant = ThemeVariant.Default;
} }
} }
public static async void CopyText(string data) { public static async void CopyText(string data)
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { {
if (desktop.MainWindow.Clipboard is { } clipbord) { if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow.Clipboard is { } clipbord)
{
await clipbord.SetTextAsync(data); await clipbord.SetTextAsync(data);
} }
} }
} }
public static string Text(string key, params object[] args) { public static string Text(string key, params object[] args)
{
var fmt = Current.FindResource($"Text.{key}") as string; var fmt = Current.FindResource($"Text.{key}") as string;
if (string.IsNullOrWhiteSpace(fmt)) return $"Text.{key}"; if (string.IsNullOrWhiteSpace(fmt)) return $"Text.{key}";
return string.Format(fmt, args); return string.Format(fmt, args);
} }
public static Avalonia.Controls.Shapes.Path CreateMenuIcon(string key) { public static Avalonia.Controls.Shapes.Path CreateMenuIcon(string key)
{
var icon = new Avalonia.Controls.Shapes.Path(); var icon = new Avalonia.Controls.Shapes.Path();
icon.Width = 12; icon.Width = 12;
icon.Height = 12; icon.Height = 12;
@ -133,29 +161,36 @@ namespace SourceGit {
return icon; return icon;
} }
public static TopLevel GetTopLevel() { public static TopLevel GetTopLevel()
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { {
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
return desktop.MainWindow; return desktop.MainWindow;
} }
return null; return null;
} }
public static void Quit() { public static void Quit()
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { {
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow.Close(); desktop.MainWindow.Close();
desktop.Shutdown(); desktop.Shutdown();
} }
} }
public override void Initialize() { public override void Initialize()
{
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
SetLocale(ViewModels.Preference.Instance.Locale); SetLocale(ViewModels.Preference.Instance.Locale);
SetTheme(ViewModels.Preference.Instance.Theme); SetTheme(ViewModels.Preference.Instance.Theme);
} }
public override void OnFrameworkInitializationCompleted() { public override void OnFrameworkInitializationCompleted()
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
BindingPlugins.DataValidators.RemoveAt(0); BindingPlugins.DataValidators.RemoveAt(0);
var launcher = new Views.Launcher(); var launcher = new Views.Launcher();

View file

@ -1,18 +1,25 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Add : Command { {
public Add(string repo, List<Models.Change> changes = null) { public class Add : Command
{
public Add(string repo, List<Models.Change> changes = null)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
if (changes == null || changes.Count == 0) { if (changes == null || changes.Count == 0)
{
Args = "add ."; Args = "add .";
} else { }
else
{
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("add --"); builder.Append("add --");
foreach (var c in changes) { foreach (var c in changes)
{
builder.Append(" \""); builder.Append(" \"");
builder.Append(c.Path); builder.Append(c.Path);
builder.Append("\""); builder.Append("\"");

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Apply : Command { {
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra) { public class Apply : Command
{
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "apply "; Args = "apply ";

View file

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

View file

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

View file

@ -2,12 +2,17 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Blame : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); public partial class Blame : Command
{
[GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")]
private static partial Regex REG_FORMAT();
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
public Blame(string repo, string file, string revision) { public Blame(string repo, string file, string revision)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"blame -t {revision} -- \"{file}\""; Args = $"blame -t {revision} -- \"{file}\"";
@ -16,15 +21,20 @@ namespace SourceGit.Commands {
_result.File = file; _result.File = file;
} }
public Models.BlameData Result() { public Models.BlameData Result()
{
var succ = Exec(); var succ = Exec();
if (!succ) { if (!succ)
{
return new Models.BlameData(); return new Models.BlameData();
} }
if (_needUnifyCommitSHA) { if (_needUnifyCommitSHA)
foreach (var line in _result.LineInfos) { {
if (line.CommitSHA.Length > _minSHALen) { foreach (var line in _result.LineInfos)
{
if (line.CommitSHA.Length > _minSHALen)
{
line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen); line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen);
} }
} }
@ -34,17 +44,19 @@ namespace SourceGit.Commands {
return _result; return _result;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
if (_result.IsBinary) return; if (_result.IsBinary) return;
if (string.IsNullOrEmpty(line)) return; if (string.IsNullOrEmpty(line)) return;
if (line.IndexOf('\0') >= 0) { if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
{
_result.IsBinary = true; _result.IsBinary = true;
_result.LineInfos.Clear(); _result.LineInfos.Clear();
return; return;
} }
var match = REG_FORMAT.Match(line); var match = REG_FORMAT().Match(line);
if (!match.Success) return; if (!match.Success) return;
_content.AppendLine(match.Groups[4].Value); _content.AppendLine(match.Groups[4].Value);
@ -54,7 +66,8 @@ namespace SourceGit.Commands {
var timestamp = int.Parse(match.Groups[3].Value); var timestamp = int.Parse(match.Groups[3].Value);
var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd"); var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd");
var info = new Models.BlameLineInfo() { var info = new Models.BlameLineInfo()
{
IsFirstInGroup = commit != _lastSHA, IsFirstInGroup = commit != _lastSHA,
CommitSHA = commit, CommitSHA = commit,
Author = author, Author = author,
@ -64,14 +77,15 @@ namespace SourceGit.Commands {
_result.LineInfos.Add(info); _result.LineInfos.Add(info);
_lastSHA = commit; _lastSHA = commit;
if (line[0] == '^') { if (line[0] == '^')
{
_needUnifyCommitSHA = true; _needUnifyCommitSHA = true;
_minSHALen = Math.Min(_minSHALen, commit.Length); _minSHALen = Math.Min(_minSHALen, commit.Length);
} }
} }
private Models.BlameData _result = new Models.BlameData(); private readonly Models.BlameData _result = new Models.BlameData();
private StringBuilder _content = new StringBuilder(); private readonly StringBuilder _content = new StringBuilder();
private string _lastSHA = string.Empty; private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false; private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64; private int _minSHALen = 64;

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public static class Branch { {
public static bool Create(string repo, string name, string basedOn) { public static class Branch
{
public static bool Create(string repo, string name, string basedOn)
{
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
@ -8,7 +11,8 @@
return cmd.Exec(); return cmd.Exec();
} }
public static bool Rename(string repo, string name, string to) { public static bool Rename(string repo, string name, string to)
{
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
@ -16,19 +20,24 @@
return cmd.Exec(); return cmd.Exec();
} }
public static bool SetUpstream(string repo, string name, string upstream) { public static bool SetUpstream(string repo, string name, string upstream)
{
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;
if (string.IsNullOrEmpty(upstream)) { if (string.IsNullOrEmpty(upstream))
{
cmd.Args = $"branch {name} --unset-upstream"; cmd.Args = $"branch {name} --unset-upstream";
} else { }
else
{
cmd.Args = $"branch {name} -u {upstream}"; cmd.Args = $"branch {name} -u {upstream}";
} }
return cmd.Exec(); return cmd.Exec();
} }
public static bool Delete(string repo, string name) { public static bool Delete(string repo, string name)
{
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = repo; cmd.WorkingDirectory = repo;
cmd.Context = repo; cmd.Context = repo;

View file

@ -2,46 +2,58 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Checkout : Command { {
public Checkout(string repo) { public class Checkout : Command
{
public Checkout(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
} }
public bool Branch(string branch, Action<string> onProgress) { public bool Branch(string branch, Action<string> onProgress)
{
Args = $"checkout --progress {branch}"; Args = $"checkout --progress {branch}";
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
_outputHandler = onProgress; _outputHandler = onProgress;
return Exec(); return Exec();
} }
public bool Branch(string branch, string basedOn, Action<string> onProgress) { public bool Branch(string branch, string basedOn, Action<string> onProgress)
{
Args = $"checkout --progress -b {branch} {basedOn}"; Args = $"checkout --progress -b {branch} {basedOn}";
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
_outputHandler = onProgress; _outputHandler = onProgress;
return Exec(); return Exec();
} }
public bool File(string file, bool useTheirs) { public bool File(string file, bool useTheirs)
if (useTheirs) { {
if (useTheirs)
{
Args = $"checkout --theirs -- \"{file}\""; Args = $"checkout --theirs -- \"{file}\"";
} else { }
else
{
Args = $"checkout --ours -- \"{file}\""; Args = $"checkout --ours -- \"{file}\"";
} }
return Exec(); return Exec();
} }
public bool FileWithRevision(string file, string revision) { public bool FileWithRevision(string file, string revision)
{
Args = $"checkout {revision} -- \"{file}\""; Args = $"checkout {revision} -- \"{file}\"";
return Exec(); return Exec();
} }
public bool Files(List<string> files) { public bool Files(List<string> files)
{
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.Append("checkout -f -q --"); builder.Append("checkout -f -q --");
foreach (var f in files) { foreach (var f in files)
{
builder.Append(" \""); builder.Append(" \"");
builder.Append(f); builder.Append(f);
builder.Append("\""); builder.Append("\"");
@ -50,7 +62,8 @@ namespace SourceGit.Commands {
return Exec(); return Exec();
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class CherryPick : Command { {
public CherryPick(string repo, string commit, bool noCommit) { public class CherryPick : Command
{
public CherryPick(string repo, string commit, bool noCommit)
{
var mode = noCommit ? "-n" : "--ff"; var mode = noCommit ? "-n" : "--ff";
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;

View file

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

View file

@ -1,17 +1,23 @@
using System; using System;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Clone : Command { {
private Action<string> _notifyProgress; public class Clone : Command
{
private readonly Action<string> _notifyProgress;
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler) { public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler)
{
Context = ctx; Context = ctx;
WorkingDirectory = path; WorkingDirectory = path;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
if (string.IsNullOrEmpty(sshKey)) { if (string.IsNullOrEmpty(sshKey))
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} else { }
else
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} }
@ -24,7 +30,8 @@ namespace SourceGit.Commands {
_notifyProgress = ouputHandler; _notifyProgress = ouputHandler;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_notifyProgress?.Invoke(line); _notifyProgress?.Invoke(line);
} }
} }

View file

@ -1,17 +1,22 @@
using Avalonia.Threading;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { using Avalonia.Threading;
public class Command {
public class CancelToken { namespace SourceGit.Commands
{
public partial class Command
{
public class CancelToken
{
public bool Requested { get; set; } = false; public bool Requested { get; set; } = false;
} }
public class ReadToEndResult { public class ReadToEndResult
{
public bool IsSuccess { get; set; } public bool IsSuccess { get; set; }
public string StdOut { get; set; } public string StdOut { get; set; }
public string StdErr { get; set; } public string StdErr { get; set; }
@ -24,7 +29,8 @@ namespace SourceGit.Commands {
public bool RaiseError { get; set; } = true; public bool RaiseError { get; set; } = true;
public bool TraitErrorAsOutput { get; set; } = false; public bool TraitErrorAsOutput { get; set; } = false;
public bool Exec() { public bool Exec()
{
var start = new ProcessStartInfo(); var start = new ProcessStartInfo();
start.FileName = Native.OS.GitInstallPath; start.FileName = Native.OS.GitInstallPath;
start.Arguments = "--no-pager -c core.quotepath=off " + Args; start.Arguments = "--no-pager -c core.quotepath=off " + Args;
@ -41,8 +47,10 @@ namespace SourceGit.Commands {
var proc = new Process() { StartInfo = start }; var proc = new Process() { StartInfo = start };
var isCancelled = false; var isCancelled = false;
proc.OutputDataReceived += (_, e) => { proc.OutputDataReceived += (_, e) =>
if (Cancel != null && Cancel.Requested) { {
if (Cancel != null && Cancel.Requested)
{
isCancelled = true; isCancelled = true;
proc.CancelErrorRead(); proc.CancelErrorRead();
proc.CancelOutputRead(); proc.CancelOutputRead();
@ -53,8 +61,10 @@ namespace SourceGit.Commands {
if (e.Data != null) OnReadline(e.Data); if (e.Data != null) OnReadline(e.Data);
}; };
proc.ErrorDataReceived += (_, e) => { proc.ErrorDataReceived += (_, e) =>
if (Cancel != null && Cancel.Requested) { {
if (Cancel != null && Cancel.Requested)
{
isCancelled = true; isCancelled = true;
proc.CancelErrorRead(); proc.CancelErrorRead();
proc.CancelOutputRead(); proc.CancelOutputRead();
@ -70,15 +80,20 @@ namespace SourceGit.Commands {
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return; if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return;
if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) return; if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) return;
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) return; if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) return;
if (_progressRegex.IsMatch(e.Data)) return; if (_progressRegex().IsMatch(e.Data)) return;
errs.Add(e.Data); errs.Add(e.Data);
}; };
try { try
{
proc.Start(); proc.Start();
} catch (Exception e) { }
if (RaiseError) { catch (Exception e)
Dispatcher.UIThread.Invoke(() => { {
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, e.Message); App.RaiseException(Context, e.Message);
}); });
} }
@ -92,19 +107,25 @@ namespace SourceGit.Commands {
int exitCode = proc.ExitCode; int exitCode = proc.ExitCode;
proc.Close(); proc.Close();
if (!isCancelled && exitCode != 0 && errs.Count > 0) { if (!isCancelled && exitCode != 0 && errs.Count > 0)
if (RaiseError) { {
Dispatcher.UIThread.Invoke(() => { if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, string.Join("\n", errs)); App.RaiseException(Context, string.Join("\n", errs));
}); });
} }
return false; return false;
} else { }
else
{
return true; return true;
} }
} }
public ReadToEndResult ReadToEnd() { public ReadToEndResult ReadToEnd()
{
var start = new ProcessStartInfo(); var start = new ProcessStartInfo();
start.FileName = Native.OS.GitInstallPath; start.FileName = Native.OS.GitInstallPath;
start.Arguments = "--no-pager -c core.quotepath=off " + Args; start.Arguments = "--no-pager -c core.quotepath=off " + Args;
@ -118,17 +139,22 @@ namespace SourceGit.Commands {
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory; if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
var proc = new Process() { StartInfo = start }; var proc = new Process() { StartInfo = start };
try { try
{
proc.Start(); proc.Start();
} catch (Exception e) { }
return new ReadToEndResult() { catch (Exception e)
{
return new ReadToEndResult()
{
IsSuccess = false, IsSuccess = false,
StdOut = string.Empty, StdOut = string.Empty,
StdErr = e.Message, StdErr = e.Message,
}; };
} }
var rs = new ReadToEndResult() { var rs = new ReadToEndResult()
{
StdOut = proc.StandardOutput.ReadToEnd(), StdOut = proc.StandardOutput.ReadToEnd(),
StdErr = proc.StandardError.ReadToEnd(), StdErr = proc.StandardError.ReadToEnd(),
}; };
@ -142,6 +168,7 @@ namespace SourceGit.Commands {
protected virtual void OnReadline(string line) { } protected virtual void OnReadline(string line) { }
private static readonly Regex _progressRegex = new Regex(@"\d+%"); [GeneratedRegex(@"\d+%")]
private static partial Regex _progressRegex();
} }
} }

View file

@ -1,8 +1,11 @@
using System.IO; using System.IO;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Commit : Command { {
public Commit(string repo, string message, bool amend, bool allowEmpty = false) { public class Commit : Command
{
public Commit(string repo, string message, bool amend, bool allowEmpty = false)
{
var file = Path.GetTempFileName(); var file = Path.GetTempFileName();
File.WriteAllText(file, message); File.WriteAllText(file, message);

View file

@ -1,30 +1,37 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class CompareRevisions : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); public partial class CompareRevisions : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
public CompareRevisions(string repo, string start, string end) { public CompareRevisions(string repo, string start, string end)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff --name-status {start} {end}"; Args = $"diff --name-status {start} {end}";
} }
public List<Models.Change> Result() { public List<Models.Change> Result()
{
Exec(); Exec();
_changes.Sort((l, r) => l.Path.CompareTo(r.Path)); _changes.Sort((l, r) => l.Path.CompareTo(r.Path));
return _changes; return _changes;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_FORMAT.Match(line); {
var match = REG_FORMAT().Match(line);
if (!match.Success) return; if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value }; var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value; var status = match.Groups[1].Value;
switch (status[0]) { switch (status[0])
{
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break; case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break; case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break; case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
@ -33,6 +40,6 @@ namespace SourceGit.Commands {
} }
} }
private List<Models.Change> _changes = new List<Models.Change>(); private readonly List<Models.Change> _changes = new List<Models.Change>();
} }
} }

View file

@ -1,29 +1,39 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Config : Command { {
public Config(string repository) { public class Config : Command
{
public Config(string repository)
{
WorkingDirectory = repository; WorkingDirectory = repository;
Context = repository; Context = repository;
RaiseError = false; RaiseError = false;
} }
public Dictionary<string, string> ListAll() { public Dictionary<string, string> ListAll()
{
Args = "config -l"; Args = "config -l";
var output = ReadToEnd(); var output = ReadToEnd();
var rs = new Dictionary<string, string>(); var rs = new Dictionary<string, string>();
if (output.IsSuccess) { if (output.IsSuccess)
{
var lines = output.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); var lines = output.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) { foreach (var line in lines)
var idx = line.IndexOf('='); {
if (idx != -1) { var idx = line.IndexOf('=', StringComparison.Ordinal);
if (idx != -1)
{
var key = line.Substring(0, idx).Trim(); var key = line.Substring(0, idx).Trim();
var val = line.Substring(idx+1).Trim(); var val = line.Substring(idx + 1).Trim();
if (rs.ContainsKey(key)) { if (rs.ContainsKey(key))
{
rs[key] = val; rs[key] = val;
} else { }
else
{
rs.Add(key, val); rs.Add(key, val);
} }
} }
@ -33,22 +43,33 @@ namespace SourceGit.Commands {
return rs; return rs;
} }
public string Get(string key) { public string Get(string key)
{
Args = $"config {key}"; Args = $"config {key}";
return ReadToEnd().StdOut.Trim(); return ReadToEnd().StdOut.Trim();
} }
public bool Set(string key, string value, bool allowEmpty = false) { public bool Set(string key, string value, bool allowEmpty = false)
if (!allowEmpty && string.IsNullOrWhiteSpace(value)) { {
if (string.IsNullOrEmpty(WorkingDirectory)) { if (!allowEmpty && string.IsNullOrWhiteSpace(value))
{
if (string.IsNullOrEmpty(WorkingDirectory))
{
Args = $"config --global --unset {key}"; Args = $"config --global --unset {key}";
} else { }
else
{
Args = $"config --unset {key}"; Args = $"config --unset {key}";
} }
} else { }
if (string.IsNullOrWhiteSpace(WorkingDirectory)) { else
{
if (string.IsNullOrWhiteSpace(WorkingDirectory))
{
Args = $"config --global {key} \"{value}\""; Args = $"config --global {key} \"{value}\"";
} else { }
else
{
Args = $"config {key} \"{value}\""; Args = $"config {key} \"{value}\"";
} }
} }

View file

@ -2,30 +2,41 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Diff : Command { {
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@"); public partial class Diff : Command
{
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR();
private static readonly string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/"; private static readonly string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/";
private static readonly string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/"; private static readonly string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/";
private static readonly string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/"; private static readonly string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/";
public Diff(string repo, Models.DiffOption opt) { public Diff(string repo, Models.DiffOption opt)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff --ignore-cr-at-eol --unified=4 {opt}"; Args = $"diff --ignore-cr-at-eol --unified=4 {opt}";
} }
public Models.DiffResult Result() { public Models.DiffResult Result()
{
Exec(); Exec();
if (_result.IsBinary || _result.IsLFS) { if (_result.IsBinary || _result.IsLFS)
{
_result.TextDiff = null; _result.TextDiff = null;
} else { }
else
{
ProcessInlineHighlights(); ProcessInlineHighlights();
if (_result.TextDiff.Lines.Count == 0) { if (_result.TextDiff.Lines.Count == 0)
{
_result.TextDiff = null; _result.TextDiff = null;
} else { }
else
{
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
} }
} }
@ -33,34 +44,47 @@ namespace SourceGit.Commands {
return _result; return _result;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
if (_result.IsBinary) return; if (_result.IsBinary) return;
if (_result.IsLFS) { if (_result.IsLFS)
{
var ch = line[0]; var ch = line[0];
if (ch == '-') { if (ch == '-')
line = line.Substring(1); {
if (line.StartsWith("oid sha256:")) { if (line.StartsWith("-oid sha256:", StringComparison.Ordinal))
_result.LFSDiff.Old.Oid = line.Substring(11); {
} else if (line.StartsWith("size ")) { _result.LFSDiff.Old.Oid = line.Substring(12);
_result.LFSDiff.Old.Size = long.Parse(line.Substring(5));
} }
} else if (ch == '+') { else if (line.StartsWith("-size ", StringComparison.Ordinal))
line = line.Substring(1); {
if (line.StartsWith("oid sha256:")) { _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
_result.LFSDiff.New.Oid = line.Substring(11);
} else if (line.StartsWith("size ")) {
_result.LFSDiff.New.Size = long.Parse(line.Substring(5));
} }
} else if (line.StartsWith(" size ")) { }
else if (ch == '+')
{
if (line.StartsWith("+oid sha256:", StringComparison.Ordinal))
{
_result.LFSDiff.New.Oid = line.Substring(12);
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = long.Parse(line.Substring(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6)); _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
} }
return; return;
} }
if (_result.TextDiff.Lines.Count == 0) { if (_result.TextDiff.Lines.Count == 0)
var match = REG_INDICATOR.Match(line); {
if (!match.Success) { var match = REG_INDICATOR().Match(line);
if (!match.Success)
{
if (line.StartsWith("Binary", StringComparison.Ordinal)) _result.IsBinary = true; if (line.StartsWith("Binary", StringComparison.Ordinal)) _result.IsBinary = true;
return; return;
} }
@ -68,8 +92,11 @@ namespace SourceGit.Commands {
_oldLine = int.Parse(match.Groups[1].Value); _oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value); _newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
} else { }
if (line.Length == 0) { else
{
if (line.Length == 0)
{
ProcessInlineHighlights(); ProcessInlineHighlights();
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine)); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
_oldLine++; _oldLine++;
@ -78,8 +105,10 @@ namespace SourceGit.Commands {
} }
var ch = line[0]; var ch = line[0];
if (ch == '-') { if (ch == '-')
if (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal)) { {
if (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal))
{
_result.IsLFS = true; _result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff(); _result.LFSDiff = new Models.LFSDiff();
return; return;
@ -87,8 +116,11 @@ namespace SourceGit.Commands {
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0)); _deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
_oldLine++; _oldLine++;
} else if (ch == '+') { }
if (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal)) { else if (ch == '+')
{
if (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal))
{
_result.IsLFS = true; _result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff(); _result.LFSDiff = new Models.LFSDiff();
return; return;
@ -96,15 +128,21 @@ namespace SourceGit.Commands {
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine)); _added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
_newLine++; _newLine++;
} else if (ch != '\\') { }
else if (ch != '\\')
{
ProcessInlineHighlights(); ProcessInlineHighlights();
var match = REG_INDICATOR.Match(line); var match = REG_INDICATOR().Match(line);
if (match.Success) { if (match.Success)
{
_oldLine = int.Parse(match.Groups[1].Value); _oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value); _newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
} else { }
if (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal)) { else
{
if (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal))
{
_result.IsLFS = true; _result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff(); _result.LFSDiff = new Models.LFSDiff();
return; return;
@ -118,10 +156,14 @@ namespace SourceGit.Commands {
} }
} }
private void ProcessInlineHighlights() { private void ProcessInlineHighlights()
if (_deleted.Count > 0) { {
if (_added.Count == _deleted.Count) { if (_deleted.Count > 0)
for (int i = _added.Count - 1; i >= 0; i--) { {
if (_added.Count == _deleted.Count)
{
for (int i = _added.Count - 1; i >= 0; i--)
{
var left = _deleted[i]; var left = _deleted[i];
var right = _added[i]; var right = _added[i];
@ -130,12 +172,15 @@ namespace SourceGit.Commands {
var chunks = Models.TextInlineChange.Compare(left.Content, right.Content); var chunks = Models.TextInlineChange.Compare(left.Content, right.Content);
if (chunks.Count > 4) continue; if (chunks.Count > 4) continue;
foreach (var chunk in chunks) { foreach (var chunk in chunks)
if (chunk.DeletedCount > 0) { {
if (chunk.DeletedCount > 0)
{
left.Highlights.Add(new Models.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount)); left.Highlights.Add(new Models.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount));
} }
if (chunk.AddedCount > 0) { if (chunk.AddedCount > 0)
{
right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount)); right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount));
} }
} }
@ -146,15 +191,16 @@ namespace SourceGit.Commands {
_deleted.Clear(); _deleted.Clear();
} }
if (_added.Count > 0) { if (_added.Count > 0)
{
_result.TextDiff.Lines.AddRange(_added); _result.TextDiff.Lines.AddRange(_added);
_added.Clear(); _added.Clear();
} }
} }
private Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() }; private readonly Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() };
private List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>(); private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>(); private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private int _oldLine = 0; private int _oldLine = 0;
private int _newLine = 0; private int _newLine = 0;
} }

View file

@ -1,38 +1,50 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Commands { namespace SourceGit.Commands
public static class Discard { {
public static void All(string repo) { public static class Discard
{
public static void All(string repo)
{
new Reset(repo, "HEAD", "--hard").Exec(); new Reset(repo, "HEAD", "--hard").Exec();
new Clean(repo).Exec(); new Clean(repo).Exec();
} }
public static void ChangesInWorkTree(string repo, List<Models.Change> changes) { public static void ChangesInWorkTree(string repo, List<Models.Change> changes)
{
var needClean = new List<string>(); var needClean = new List<string>();
var needCheckout = new List<string>(); var needCheckout = new List<string>();
foreach (var c in changes) { foreach (var c in changes)
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) { {
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
{
needClean.Add(c.Path); needClean.Add(c.Path);
} else { }
else
{
needCheckout.Add(c.Path); needCheckout.Add(c.Path);
} }
} }
for (int i = 0; i < needClean.Count; i += 10) { for (int i = 0; i < needClean.Count; i += 10)
{
var count = Math.Min(10, needClean.Count - i); var count = Math.Min(10, needClean.Count - i);
new Clean(repo, needClean.GetRange(i, count)).Exec(); new Clean(repo, needClean.GetRange(i, count)).Exec();
} }
for (int i = 0; i < needCheckout.Count; i += 10) { for (int i = 0; i < needCheckout.Count; i += 10)
{
var count = Math.Min(10, needCheckout.Count - i); var count = Math.Min(10, needCheckout.Count - i);
new Checkout(repo).Files(needCheckout.GetRange(i, count)); new Checkout(repo).Files(needCheckout.GetRange(i, count));
} }
} }
public static void ChangesInStaged(string repo, List<Models.Change> changes) { public static void ChangesInStaged(string repo, List<Models.Change> changes)
for (int i = 0; i < changes.Count; i += 10) { {
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i); var count = Math.Min(10, changes.Count - i);
var files = new List<string>(); var files = new List<string>();
for (int j = 0; j < count; j++) files.Add(changes[i + j].Path); for (int j = 0; j < count; j++) files.Add(changes[i + j].Path);

View file

@ -3,18 +3,24 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Fetch : Command { {
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler) { public class Fetch : Command
{
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler)
{
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) { if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else { }
else
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
@ -25,59 +31,75 @@ namespace SourceGit.Commands {
AutoFetch.MarkFetched(repo); AutoFetch.MarkFetched(repo);
} }
public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action<string> outputHandler) { public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action<string> outputHandler)
{
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) { if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else { }
else
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}"; Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}";
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }
private Action<string> _outputHandler; private readonly Action<string> _outputHandler;
} }
public class AutoFetch { public class AutoFetch
public static bool IsEnabled { {
public static bool IsEnabled
{
get; get;
set; set;
} = false; } = false;
class Job { class Job
{
public Fetch Cmd = null; public Fetch Cmd = null;
public DateTime NextRunTimepoint = DateTime.MinValue; public DateTime NextRunTimepoint = DateTime.MinValue;
} }
static AutoFetch() { static AutoFetch()
Task.Run(() => { {
while (true) { Task.Run(() =>
if (!IsEnabled) { {
while (true)
{
if (!IsEnabled)
{
Thread.Sleep(10000); Thread.Sleep(10000);
continue; continue;
} }
var now = DateTime.Now; var now = DateTime.Now;
var uptodate = new List<Job>(); var uptodate = new List<Job>();
lock (_lock) { lock (_lock)
foreach (var job in _jobs) { {
if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0) { foreach (var job in _jobs)
{
if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0)
{
uptodate.Add(job.Value); uptodate.Add(job.Value);
} }
} }
} }
foreach (var job in uptodate) { foreach (var job in uptodate)
{
job.Cmd.Exec(); job.Cmd.Exec();
job.NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval); job.NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval);
} }
@ -87,37 +109,48 @@ namespace SourceGit.Commands {
}); });
} }
public static void AddRepository(string repo) { public static void AddRepository(string repo)
var job = new Job { {
var job = new Job
{
Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false }, Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false },
NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval), NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval),
}; };
lock (_lock) { lock (_lock)
if (_jobs.ContainsKey(repo)) { {
if (_jobs.ContainsKey(repo))
{
_jobs[repo] = job; _jobs[repo] = job;
} else { }
else
{
_jobs.Add(repo, job); _jobs.Add(repo, job);
} }
} }
} }
public static void RemoveRepository(string repo) { public static void RemoveRepository(string repo)
lock (_lock) { {
lock (_lock)
{
_jobs.Remove(repo); _jobs.Remove(repo);
} }
} }
public static void MarkFetched(string repo) { public static void MarkFetched(string repo)
lock (_lock) { {
if (_jobs.ContainsKey(repo)) { lock (_lock)
{
if (_jobs.ContainsKey(repo))
{
_jobs[repo].NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval); _jobs[repo].NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval);
} }
} }
} }
private static Dictionary<string, Job> _jobs = new Dictionary<string, Job>(); private static readonly Dictionary<string, Job> _jobs = new Dictionary<string, Job>();
private static object _lock = new object(); private static readonly object _lock = new object();
private static double _fetchInterval = 10 * 60; private static readonly double _fetchInterval = 10 * 60;
} }
} }

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class FormatPatch : Command { {
public FormatPatch(string repo, string commit, string saveTo) { public class FormatPatch : Command
{
public FormatPatch(string repo, string commit, string saveTo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"format-patch {commit} -1 -o \"{saveTo}\""; Args = $"format-patch {commit} -1 -o \"{saveTo}\"";

View file

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

View file

@ -1,14 +1,19 @@
using Avalonia.Threading; using System.Collections.Generic;
using System.Collections.Generic;
namespace SourceGit.Commands { using Avalonia.Threading;
public class GitFlow : Command {
public GitFlow(string repo) { namespace SourceGit.Commands
{
public class GitFlow : Command
{
public GitFlow(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
} }
public bool Init(List<Models.Branch> branches, string master, string develop, string feature, string release, string hotfix, string version) { public bool Init(List<Models.Branch> branches, string master, string develop, string feature, string release, string hotfix, string version)
{
var current = branches.Find(x => x.IsCurrent); var current = branches.Find(x => x.IsCurrent);
var masterBranch = branches.Find(x => x.Name == master); var masterBranch = branches.Find(x => x.Name == master);
@ -31,8 +36,10 @@ namespace SourceGit.Commands {
return Exec(); return Exec();
} }
public bool Start(Models.GitFlowBranchType type, string name) { public bool Start(Models.GitFlowBranchType type, string name)
switch (type) { {
switch (type)
{
case Models.GitFlowBranchType.Feature: case Models.GitFlowBranchType.Feature:
Args = $"flow feature start {name}"; Args = $"flow feature start {name}";
break; break;
@ -43,7 +50,8 @@ namespace SourceGit.Commands {
Args = $"flow hotfix start {name}"; Args = $"flow hotfix start {name}";
break; break;
default: default:
Dispatcher.UIThread.Invoke(() => { Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, "Bad branch type!!!"); App.RaiseException(Context, "Bad branch type!!!");
}); });
return false; return false;
@ -52,9 +60,11 @@ namespace SourceGit.Commands {
return Exec(); return Exec();
} }
public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch) { public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch)
{
var option = keepBranch ? "-k" : string.Empty; var option = keepBranch ? "-k" : string.Empty;
switch (type) { switch (type)
{
case Models.GitFlowBranchType.Feature: case Models.GitFlowBranchType.Feature:
Args = $"flow feature finish {option} {name}"; Args = $"flow feature finish {option} {name}";
break; break;
@ -65,7 +75,8 @@ namespace SourceGit.Commands {
Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\""; Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\"";
break; break;
default: default:
Dispatcher.UIThread.Invoke(() => { Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, "Bad branch type!!!"); App.RaiseException(Context, "Bad branch type!!!");
}); });
return false; return false;

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Init : Command { {
public Init(string ctx, string dir) { public class Init : Command
{
public Init(string ctx, string dir)
{
Context = ctx; Context = ctx;
WorkingDirectory = dir; WorkingDirectory = dir;
Args = "init -q"; Args = "init -q";

View file

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

View file

@ -1,13 +1,17 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class IsLFSFiltered : Command { {
public IsLFSFiltered(string repo, string path) { public class IsLFSFiltered : Command
{
public IsLFSFiltered(string repo, string path)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"check-attr -a -z \"{path}\""; Args = $"check-attr -a -z \"{path}\"";
RaiseError = false; RaiseError = false;
} }
public bool Result() { public bool Result()
{
var rs = ReadToEnd(); var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Contains("filter\0lfs"); return rs.IsSuccess && rs.StdOut.Contains("filter\0lfs");
} }

View file

@ -1,10 +1,14 @@
using System; using System;
using System.IO; using System.IO;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class LFS { {
class PruneCmd : Command { public class LFS
public PruneCmd(string repo, Action<string> onProgress) { {
class PruneCmd : Command
{
public PruneCmd(string repo, Action<string> onProgress)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "lfs prune"; Args = "lfs prune";
@ -12,18 +16,21 @@ namespace SourceGit.Commands {
_outputHandler = onProgress; _outputHandler = onProgress;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }
private Action<string> _outputHandler; private readonly Action<string> _outputHandler;
} }
public LFS(string repo) { public LFS(string repo)
{
_repo = repo; _repo = repo;
} }
public bool IsEnabled() { public bool IsEnabled()
{
var path = Path.Combine(_repo, ".git", "hooks", "pre-push"); var path = Path.Combine(_repo, ".git", "hooks", "pre-push");
if (!File.Exists(path)) return false; if (!File.Exists(path)) return false;
@ -31,10 +38,11 @@ namespace SourceGit.Commands {
return content.Contains("git lfs pre-push"); return content.Contains("git lfs pre-push");
} }
public void Prune(Action<string> outputHandler) { public void Prune(Action<string> outputHandler)
{
new PruneCmd(_repo, outputHandler).Exec(); new PruneCmd(_repo, outputHandler).Exec();
} }
private string _repo; private readonly string _repo;
} }
} }

View file

@ -1,8 +1,11 @@
using System; using System;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Merge : Command { {
public Merge(string repo, string source, string mode, Action<string> outputHandler) { public class Merge : Command
{
public Merge(string repo, string source, string mode, Action<string> outputHandler)
{
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
@ -10,10 +13,11 @@ namespace SourceGit.Commands {
Args = $"merge --progress {source} {mode}"; Args = $"merge --progress {source} {mode}";
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }
private Action<string> _outputHandler = null; private readonly Action<string> _outputHandler = null;
} }
} }

View file

@ -1,18 +1,26 @@
using Avalonia.Threading; using System.IO;
using System.IO;
namespace SourceGit.Commands { using Avalonia.Threading;
public static class MergeTool {
public static bool OpenForMerge(string repo, string tool, string mergeCmd, string file) { namespace SourceGit.Commands
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(mergeCmd)) { {
Dispatcher.UIThread.Invoke(() => { public static class MergeTool
{
public static bool OpenForMerge(string repo, string tool, string mergeCmd, string file)
{
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(mergeCmd))
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Invalid external merge tool settings!"); App.RaiseException(repo, "Invalid external merge tool settings!");
}); });
return false; return false;
} }
if (!File.Exists(tool)) { if (!File.Exists(tool))
Dispatcher.UIThread.Invoke(() => { {
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!"); App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!");
}); });
return false; return false;
@ -25,16 +33,21 @@ namespace SourceGit.Commands {
return cmd.Exec(); return cmd.Exec();
} }
public static bool OpenForDiff(string repo, string tool, string diffCmd, Models.DiffOption option) { public static bool OpenForDiff(string repo, string tool, string diffCmd, Models.DiffOption option)
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(diffCmd)) { {
Dispatcher.UIThread.Invoke(() => { if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(diffCmd))
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Invalid external merge tool settings!"); App.RaiseException(repo, "Invalid external merge tool settings!");
}); });
return false; return false;
} }
if (!File.Exists(tool)) { if (!File.Exists(tool))
Dispatcher.UIThread.Invoke(() => { {
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!"); App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!");
}); });
return false; return false;

View file

@ -1,17 +1,23 @@
using System; using System;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Pull : Command { {
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler) { public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler)
{
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) { if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else { }
else
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
@ -20,10 +26,11 @@ namespace SourceGit.Commands {
Args += $"{remote} {branch}"; Args += $"{remote} {branch}";
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }
private Action<string> _outputHandler; private readonly Action<string> _outputHandler;
} }
} }

View file

@ -1,17 +1,23 @@
using System; using System;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Push : Command { {
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress) { public class Push : Command
{
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
_outputHandler = onProgress; _outputHandler = onProgress;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) { if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else { }
else
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
@ -30,29 +36,37 @@ namespace SourceGit.Commands {
/// <param name="repo"></param> /// <param name="repo"></param>
/// <param name="remote"></param> /// <param name="remote"></param>
/// <param name="branch"></param> /// <param name="branch"></param>
public Push(string repo, string remote, string branch) { public Push(string repo, string remote, string branch)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) { if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else { }
else
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
Args += $"push {remote} --delete {branch}"; Args += $"push {remote} --delete {branch}";
} }
public Push(string repo, string remote, string tag, bool isDelete) { public Push(string repo, string remote, string tag, bool isDelete)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) { if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else { }
else
{
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
@ -61,10 +75,11 @@ namespace SourceGit.Commands {
Args += $"{remote} refs/tags/{tag}"; Args += $"{remote} refs/tags/{tag}";
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }
private Action<string> _outputHandler = null; private readonly Action<string> _outputHandler = null;
} }
} }

View file

@ -2,26 +2,37 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryBranches : Command { {
public partial class QueryBranches : Command
{
private static readonly string PREFIX_LOCAL = "refs/heads/"; private static readonly string PREFIX_LOCAL = "refs/heads/";
private static readonly string PREFIX_REMOTE = "refs/remotes/"; private static readonly string PREFIX_REMOTE = "refs/remotes/";
private static readonly Regex REG_AHEAD_BEHIND = new Regex(@"^(\d+)\s(\d+)$");
public QueryBranches(string repo) { [GeneratedRegex(@"^(\d+)\s(\d+)$")]
private static partial Regex REG_AHEAD_BEHIND();
public QueryBranches(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:trackshort)\""; Args = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:trackshort)\"";
} }
public List<Models.Branch> Result() { public List<Models.Branch> Result()
{
Exec(); Exec();
foreach (var b in _branches) { foreach (var b in _branches)
if (b.IsLocal && !string.IsNullOrEmpty(b.UpstreamTrackStatus)) { {
if (b.UpstreamTrackStatus == "=") { if (b.IsLocal && !string.IsNullOrEmpty(b.UpstreamTrackStatus))
{
if (b.UpstreamTrackStatus == "=")
{
b.UpstreamTrackStatus = string.Empty; b.UpstreamTrackStatus = string.Empty;
} else { }
else
{
b.UpstreamTrackStatus = ParseTrackStatus(b.Name, b.Upstream); b.UpstreamTrackStatus = ParseTrackStatus(b.Name, b.Upstream);
} }
} }
@ -30,26 +41,32 @@ namespace SourceGit.Commands {
return _branches; return _branches;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
var parts = line.Split('$'); var parts = line.Split('$');
if (parts.Length != 5) return; if (parts.Length != 5) return;
var branch = new Models.Branch(); var branch = new Models.Branch();
var refName = parts[0]; var refName = parts[0];
if (refName.EndsWith("/HEAD")) return; if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) return;
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal))
{
branch.Name = refName.Substring(PREFIX_LOCAL.Length); branch.Name = refName.Substring(PREFIX_LOCAL.Length);
branch.IsLocal = true; branch.IsLocal = true;
} else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal)) { }
else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal))
{
var name = refName.Substring(PREFIX_REMOTE.Length); var name = refName.Substring(PREFIX_REMOTE.Length);
var shortNameIdx = name.IndexOf('/'); var shortNameIdx = name.IndexOf('/', StringComparison.Ordinal);
if (shortNameIdx < 0) return; if (shortNameIdx < 0) return;
branch.Remote = name.Substring(0, shortNameIdx); branch.Remote = name.Substring(0, shortNameIdx);
branch.Name = name.Substring(branch.Remote.Length + 1); branch.Name = name.Substring(branch.Remote.Length + 1);
branch.IsLocal = false; branch.IsLocal = false;
} else { }
else
{
branch.Name = refName; branch.Name = refName;
branch.IsLocal = true; branch.IsLocal = true;
} }
@ -62,7 +79,8 @@ namespace SourceGit.Commands {
_branches.Add(branch); _branches.Add(branch);
} }
private string ParseTrackStatus(string local, string upstream) { private string ParseTrackStatus(string local, string upstream)
{
var cmd = new Command(); var cmd = new Command();
cmd.WorkingDirectory = WorkingDirectory; cmd.WorkingDirectory = WorkingDirectory;
cmd.Context = Context; cmd.Context = Context;
@ -71,7 +89,7 @@ namespace SourceGit.Commands {
var rs = cmd.ReadToEnd(); var rs = cmd.ReadToEnd();
if (!rs.IsSuccess) return string.Empty; if (!rs.IsSuccess) return string.Empty;
var match = REG_AHEAD_BEHIND.Match(rs.StdOut); var match = REG_AHEAD_BEHIND().Match(rs.StdOut);
if (!match.Success) return string.Empty; if (!match.Success) return string.Empty;
var ahead = int.Parse(match.Groups[1].Value); var ahead = int.Parse(match.Groups[1].Value);
@ -82,6 +100,6 @@ namespace SourceGit.Commands {
return track.Trim(); return track.Trim();
} }
private List<Models.Branch> _branches = new List<Models.Branch>(); private readonly List<Models.Branch> _branches = new List<Models.Branch>();
} }
} }

View file

@ -1,30 +1,37 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryCommitChanges : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); public partial class QueryCommitChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
public QueryCommitChanges(string repo, string commitSHA) { public QueryCommitChanges(string repo, string commitSHA)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"show --name-status {commitSHA}"; Args = $"show --name-status {commitSHA}";
} }
public List<Models.Change> Result() { public List<Models.Change> Result()
{
Exec(); Exec();
_changes.Sort((l, r) => l.Path.CompareTo(r.Path)); _changes.Sort((l, r) => l.Path.CompareTo(r.Path));
return _changes; return _changes;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_FORMAT.Match(line); {
var match = REG_FORMAT().Match(line);
if (!match.Success) return; if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value }; var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value; var status = match.Groups[1].Value;
switch (status[0]) { switch (status[0])
{
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break; case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break; case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break; case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
@ -33,6 +40,6 @@ namespace SourceGit.Commands {
} }
} }
private List<Models.Change> _changes = new List<Models.Change>(); private readonly List<Models.Change> _changes = new List<Models.Change>();
} }
} }

View file

@ -1,49 +1,61 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryCommits : Command { {
public class QueryCommits : Command
{
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----"; private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
private List<Models.Commit> commits = new List<Models.Commit>(); private readonly List<Models.Commit> commits = new List<Models.Commit>();
private Models.Commit current = null; private Models.Commit current = null;
private bool isSkipingGpgsig = false; private bool isSkipingGpgsig = false;
private bool isHeadFounded = false; private bool isHeadFounded = false;
private bool findFirstMerged = true; private readonly bool findFirstMerged = true;
public QueryCommits(string repo, string limits, bool needFindHead = true) { public QueryCommits(string repo, string limits, bool needFindHead = true)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Args = "log --date-order --decorate=full --pretty=raw " + limits; Args = "log --date-order --decorate=full --pretty=raw " + limits;
findFirstMerged = needFindHead; findFirstMerged = needFindHead;
} }
public List<Models.Commit> Result() { public List<Models.Commit> Result()
{
Exec(); Exec();
if (current != null) { if (current != null)
{
current.Message = current.Message.Trim(); current.Message = current.Message.Trim();
commits.Add(current); commits.Add(current);
} }
if (findFirstMerged && !isHeadFounded && commits.Count > 0) { if (findFirstMerged && !isHeadFounded && commits.Count > 0)
{
MarkFirstMerged(); MarkFirstMerged();
} }
return commits; return commits;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
if (isSkipingGpgsig) { {
if (isSkipingGpgsig)
{
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false; if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false;
return; return;
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) { }
else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal))
{
isSkipingGpgsig = true; isSkipingGpgsig = true;
return; return;
} }
if (line.StartsWith("commit ", StringComparison.Ordinal)) { if (line.StartsWith("commit ", StringComparison.Ordinal))
if (current != null) { {
if (current != null)
{
current.Message = current.Message.Trim(); current.Message = current.Message.Trim();
commits.Add(current); commits.Add(current);
} }
@ -51,10 +63,13 @@ namespace SourceGit.Commands {
current = new Models.Commit(); current = new Models.Commit();
line = line.Substring(7); line = line.Substring(7);
var decoratorStart = line.IndexOf('('); var decoratorStart = line.IndexOf('(', StringComparison.Ordinal);
if (decoratorStart < 0) { if (decoratorStart < 0)
{
current.SHA = line.Trim(); current.SHA = line.Trim();
} else { }
else
{
current.SHA = line.Substring(0, decoratorStart).Trim(); current.SHA = line.Substring(0, decoratorStart).Trim();
current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1)); current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1));
if (!isHeadFounded) isHeadFounded = current.IsMerged; if (!isHeadFounded) isHeadFounded = current.IsMerged;
@ -65,65 +80,95 @@ namespace SourceGit.Commands {
if (current == null) return; if (current == null) return;
if (line.StartsWith("tree ", StringComparison.Ordinal)) { if (line.StartsWith("tree ", StringComparison.Ordinal))
{
return; return;
} else if (line.StartsWith("parent ", StringComparison.Ordinal)) { }
else if (line.StartsWith("parent ", StringComparison.Ordinal))
{
current.Parents.Add(line.Substring("parent ".Length)); current.Parents.Add(line.Substring("parent ".Length));
} else if (line.StartsWith("author ", StringComparison.Ordinal)) { }
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid; Models.User user = Models.User.Invalid;
ulong time = 0; ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
current.Author = user; current.Author = user;
current.AuthorTime = time; current.AuthorTime = time;
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) { }
else if (line.StartsWith("committer ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid; Models.User user = Models.User.Invalid;
ulong time = 0; ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time); Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time);
current.Committer = user; current.Committer = user;
current.CommitterTime = time; current.CommitterTime = time;
} else if (string.IsNullOrEmpty(current.Subject)) { }
else if (string.IsNullOrEmpty(current.Subject))
{
current.Subject = line.Trim(); current.Subject = line.Trim();
} else { }
else
{
current.Message += (line.Trim() + "\n"); current.Message += (line.Trim() + "\n");
} }
} }
private bool ParseDecorators(List<Models.Decorator> decorators, string data) { private bool ParseDecorators(List<Models.Decorator> decorators, string data)
{
bool isHeadOfCurrent = false; bool isHeadOfCurrent = false;
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs) { foreach (var sub in subs)
{
var d = sub.Trim(); var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal))
decorators.Add(new Models.Decorator() { {
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.Tag, Type = Models.DecoratorType.Tag,
Name = d.Substring(15).Trim(), Name = d.Substring(15).Trim(),
}); });
} else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) { }
else if (d.EndsWith("/HEAD", StringComparison.Ordinal))
{
continue; continue;
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { }
else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal))
{
isHeadOfCurrent = true; isHeadOfCurrent = true;
decorators.Add(new Models.Decorator() { decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentBranchHead, Type = Models.DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim(), Name = d.Substring(19).Trim(),
}); });
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { }
decorators.Add(new Models.Decorator() { else if (d.StartsWith("refs/heads/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.LocalBranchHead, Type = Models.DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim(), Name = d.Substring(11).Trim(),
}); });
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) { }
decorators.Add(new Models.Decorator() { else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.RemoteBranchHead, Type = Models.DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim(), Name = d.Substring(13).Trim(),
}); });
} }
} }
decorators.Sort((l, r) => { decorators.Sort((l, r) =>
if (l.Type != r.Type) { {
if (l.Type != r.Type)
{
return (int)l.Type - (int)r.Type; return (int)l.Type - (int)r.Type;
} else { }
else
{
return l.Name.CompareTo(r.Name); return l.Name.CompareTo(r.Name);
} }
}); });
@ -131,7 +176,8 @@ namespace SourceGit.Commands {
return isHeadOfCurrent; return isHeadOfCurrent;
} }
private void MarkFirstMerged() { private void MarkFirstMerged()
{
Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\""; Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd(); var rs = ReadToEnd();
@ -141,8 +187,10 @@ namespace SourceGit.Commands {
var set = new HashSet<string>(); var set = new HashSet<string>();
foreach (var sha in shas) set.Add(sha); foreach (var sha in shas) set.Add(sha);
foreach (var c in commits) { foreach (var c in commits)
if (set.Contains(c.SHA)) { {
if (set.Contains(c.SHA))
{
c.IsMerged = true; c.IsMerged = true;
break; break;
} }

View file

@ -1,23 +1,28 @@
using System.Text; using System.Text;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryFileContent : Command { {
public QueryFileContent(string repo, string revision, string file) { public class QueryFileContent : Command
{
public QueryFileContent(string repo, string revision, string file)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"show {revision}:\"{file}\""; Args = $"show {revision}:\"{file}\"";
} }
public string Result() { public string Result()
{
Exec(); Exec();
return _builder.ToString(); return _builder.ToString();
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_builder.Append(line); _builder.Append(line);
_builder.Append('\n'); _builder.Append('\n');
} }
private StringBuilder _builder = new StringBuilder(); private readonly StringBuilder _builder = new StringBuilder();
} }
} }

View file

@ -1,22 +1,30 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryFileSize : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$"); public partial class QueryFileSize : Command
{
public QueryFileSize(string repo, string file, string revision) { [GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")]
private static partial Regex REG_FORMAT();
public QueryFileSize(string repo, string file, string revision)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"ls-tree {revision} -l -- {file}"; Args = $"ls-tree {revision} -l -- {file}";
} }
public long Result() { public long Result()
{
if (_result != 0) return _result; if (_result != 0) return _result;
var rs = ReadToEnd(); var rs = ReadToEnd();
if (rs.IsSuccess) { if (rs.IsSuccess)
var match = REG_FORMAT.Match(rs.StdOut); {
if (match.Success) { var match = REG_FORMAT().Match(rs.StdOut);
if (match.Success)
{
return long.Parse(match.Groups[1].Value); return long.Parse(match.Groups[1].Value);
} }
} }
@ -24,6 +32,6 @@ namespace SourceGit.Commands {
return 0; return 0;
} }
private long _result = 0; private readonly long _result = 0;
} }
} }

View file

@ -1,14 +1,18 @@
using System.IO; using System.IO;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryGitDir : Command { {
public QueryGitDir(string workDir) { public class QueryGitDir : Command
{
public QueryGitDir(string workDir)
{
WorkingDirectory = workDir; WorkingDirectory = workDir;
Args = "rev-parse --git-dir"; Args = "rev-parse --git-dir";
RaiseError = false; RaiseError = false;
} }
public string Result() { public string Result()
{
var rs = ReadToEnd().StdOut; var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs)) return null; if (string.IsNullOrEmpty(rs)) return null;

View file

@ -2,31 +2,38 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryLocalChanges : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); public partial class QueryLocalChanges : Command
private static readonly string[] UNTRACKED = [ "no", "all" ]; {
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
private static readonly string[] UNTRACKED = ["no", "all"];
public QueryLocalChanges(string repo, bool includeUntracked = true) { public QueryLocalChanges(string repo, bool includeUntracked = true)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
} }
public List<Models.Change> Result() { public List<Models.Change> Result()
{
Exec(); Exec();
return _changes; return _changes;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_FORMAT.Match(line); {
var match = REG_FORMAT().Match(line);
if (!match.Success) return; if (!match.Success) return;
if (line.EndsWith("/", StringComparison.Ordinal)) return; // Ignore changes with git-worktree if (line.EndsWith("/", StringComparison.Ordinal)) return; // Ignore changes with git-worktree
var change = new Models.Change() { Path = match.Groups[2].Value }; var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value; var status = match.Groups[1].Value;
switch (status) { switch (status)
{
case " M": change.Set(Models.ChangeState.None, Models.ChangeState.Modified); break; case " M": change.Set(Models.ChangeState.None, Models.ChangeState.Modified); break;
case " A": change.Set(Models.ChangeState.None, Models.ChangeState.Added); break; case " A": change.Set(Models.ChangeState.None, Models.ChangeState.Added); break;
case " D": change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); break; case " D": change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); break;
@ -61,6 +68,6 @@ namespace SourceGit.Commands {
_changes.Add(change); _changes.Add(change);
} }
private List<Models.Change> _changes = new List<Models.Change>(); private readonly List<Models.Change> _changes = new List<Models.Change>();
} }
} }

View file

@ -1,26 +1,33 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryRemotes : Command { {
private static readonly Regex REG_REMOTE = new Regex(@"^([\w\.\-]+)\s*(\S+).*$"); public partial class QueryRemotes : Command
{
[GeneratedRegex(@"^([\w\.\-]+)\s*(\S+).*$")]
private static partial Regex REG_REMOTE();
public QueryRemotes(string repo) { public QueryRemotes(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "remote -v"; Args = "remote -v";
} }
public List<Models.Remote> Result() { public List<Models.Remote> Result()
{
Exec(); Exec();
return _loaded; return _loaded;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_REMOTE.Match(line); {
var match = REG_REMOTE().Match(line);
if (!match.Success) return; if (!match.Success) return;
var remote = new Models.Remote() { var remote = new Models.Remote()
{
Name = match.Groups[1].Value, Name = match.Groups[1].Value,
URL = match.Groups[2].Value, URL = match.Groups[2].Value,
}; };
@ -29,6 +36,6 @@ namespace SourceGit.Commands {
_loaded.Add(remote); _loaded.Add(remote);
} }
private List<Models.Remote> _loaded = new List<Models.Remote>(); private readonly List<Models.Remote> _loaded = new List<Models.Remote>();
} }
} }

View file

@ -1,12 +1,16 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryRepositoryRootPath : Command { {
public QueryRepositoryRootPath(string path) { public class QueryRepositoryRootPath : Command
{
public QueryRepositoryRootPath(string path)
{
WorkingDirectory = path; WorkingDirectory = path;
Args = "rev-parse --show-toplevel"; Args = "rev-parse --show-toplevel";
RaiseError = false; RaiseError = false;
} }
public string Result() { public string Result()
{
var rs = ReadToEnd().StdOut; var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs)) return null; if (string.IsNullOrEmpty(rs)) return null;
return rs.Trim(); return rs.Trim();

View file

@ -1,24 +1,31 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryRevisionObjects : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$"); public partial class QueryRevisionObjects : Command
private List<Models.Object> objects = new List<Models.Object>(); {
public QueryRevisionObjects(string repo, string sha) { [GeneratedRegex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$")]
private static partial Regex REG_FORMAT();
private readonly List<Models.Object> objects = new List<Models.Object>();
public QueryRevisionObjects(string repo, string sha)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"ls-tree -r {sha}"; Args = $"ls-tree -r {sha}";
} }
public List<Models.Object> Result() { public List<Models.Object> Result()
{
Exec(); Exec();
return objects; return objects;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_FORMAT.Match(line); {
var match = REG_FORMAT().Match(line);
if (!match.Success) return; if (!match.Success) return;
var obj = new Models.Object(); var obj = new Models.Object();
@ -26,7 +33,8 @@ namespace SourceGit.Commands {
obj.Type = Models.ObjectType.Blob; obj.Type = Models.ObjectType.Blob;
obj.Path = match.Groups[3].Value; obj.Path = match.Groups[3].Value;
switch (match.Groups[1].Value) { switch (match.Groups[1].Value)
{
case "blob": obj.Type = Models.ObjectType.Blob; break; case "blob": obj.Type = Models.ObjectType.Blob; break;
case "tree": obj.Type = Models.ObjectType.Tree; break; case "tree": obj.Type = Models.ObjectType.Tree; break;
case "tag": obj.Type = Models.ObjectType.Tag; break; case "tag": obj.Type = Models.ObjectType.Tag; break;

View file

@ -1,19 +1,25 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryStagedFileBlobGuid : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+([0-9a-f]+)\s+.*$"); public partial class QueryStagedFileBlobGuid : Command
{
[GeneratedRegex(@"^\d+\s+([0-9a-f]+)\s+.*$")]
private static partial Regex REG_FORMAT();
public QueryStagedFileBlobGuid(string repo, string file) { public QueryStagedFileBlobGuid(string repo, string file)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"ls-files -s -- \"{file}\""; Args = $"ls-files -s -- \"{file}\"";
} }
public string Result() { public string Result()
{
var rs = ReadToEnd(); var rs = ReadToEnd();
var match = REG_FORMAT.Match(rs.StdOut.Trim()); var match = REG_FORMAT().Match(rs.StdOut.Trim());
if (match.Success) { if (match.Success)
{
return match.Groups[1].Value; return match.Groups[1].Value;
} }

View file

@ -1,29 +1,37 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryStashChanges : Command { {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); public partial class QueryStashChanges : Command
{
public QueryStashChanges(string repo, string sha) { [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
public QueryStashChanges(string repo, string sha)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff --name-status --pretty=format: {sha}^ {sha}"; Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
} }
public List<Models.Change> Result() { public List<Models.Change> Result()
{
Exec(); Exec();
return _changes; return _changes;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_FORMAT.Match(line); {
var match = REG_FORMAT().Match(line);
if (!match.Success) return; if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value }; var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value; var status = match.Groups[1].Value;
switch (status[0]) { switch (status[0])
{
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break; case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break; case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break; case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
@ -32,6 +40,6 @@ namespace SourceGit.Commands {
} }
} }
private List<Models.Change> _changes = new List<Models.Change>(); private readonly List<Models.Change> _changes = new List<Models.Change>();
} }
} }

View file

@ -2,24 +2,32 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryStashes : Command { {
private static readonly Regex REG_STASH = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$"); public partial class QueryStashes : Command
{
public QueryStashes(string repo) { [GeneratedRegex(@"^Reflog: refs/(stash@\{\d+\}).*$")]
private static partial Regex REG_STASH();
public QueryStashes(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "stash list --pretty=raw"; Args = "stash list --pretty=raw";
} }
public List<Models.Stash> Result() { public List<Models.Stash> Result()
{
Exec(); Exec();
if (_current != null) _stashes.Add(_current); if (_current != null) _stashes.Add(_current);
return _stashes; return _stashes;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
if (line.StartsWith("commit ", StringComparison.Ordinal)) { {
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
if (_current != null && !string.IsNullOrEmpty(_current.Name)) _stashes.Add(_current); if (_current != null && !string.IsNullOrEmpty(_current.Name)) _stashes.Add(_current);
_current = new Models.Stash() { SHA = line.Substring(7, 8) }; _current = new Models.Stash() { SHA = line.Substring(7, 8) };
return; return;
@ -27,12 +35,17 @@ namespace SourceGit.Commands {
if (_current == null) return; if (_current == null) return;
if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) { if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal))
var match = REG_STASH.Match(line); {
var match = REG_STASH().Match(line);
if (match.Success) _current.Name = match.Groups[1].Value; if (match.Success) _current.Name = match.Groups[1].Value;
} else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal)) { }
else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal))
{
_current.Message = line.Substring(16); _current.Message = line.Substring(16);
} else if (line.StartsWith("author ", StringComparison.Ordinal)) { }
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid; Models.User user = Models.User.Invalid;
ulong time = 0; ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
@ -41,7 +54,7 @@ namespace SourceGit.Commands {
} }
} }
private List<Models.Stash> _stashes = new List<Models.Stash>(); private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null; private Models.Stash _current = null;
} }
} }

View file

@ -1,35 +1,44 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QuerySubmodules : Command { {
private readonly Regex REG_FORMAT1 = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$"); public partial class QuerySubmodules : Command
private readonly Regex REG_FORMAT2 = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)$"); {
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
private static partial Regex REG_FORMAT2();
public QuerySubmodules(string repo) { public QuerySubmodules(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "submodule status"; Args = "submodule status";
} }
public List<string> Result() { public List<string> Result()
{
Exec(); Exec();
return _submodules; return _submodules;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
var match = REG_FORMAT1.Match(line); {
if (match.Success) { var match = REG_FORMAT1().Match(line);
if (match.Success)
{
_submodules.Add(match.Groups[1].Value); _submodules.Add(match.Groups[1].Value);
return; return;
} }
match = REG_FORMAT2.Match(line); match = REG_FORMAT2().Match(line);
if (match.Success) { if (match.Success)
{
_submodules.Add(match.Groups[1].Value); _submodules.Add(match.Groups[1].Value);
} }
} }
private List<string> _submodules = new List<string>(); private readonly List<string> _submodules = new List<string>();
} }
} }

View file

@ -1,34 +1,44 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class QueryTags : Command { {
public QueryTags(string repo) { public class QueryTags : Command
{
public QueryTags(string repo)
{
Context = repo; Context = repo;
WorkingDirectory = repo; WorkingDirectory = repo;
Args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags"; Args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
} }
public List<Models.Tag> Result() { public List<Models.Tag> Result()
{
Exec(); Exec();
return _loaded; return _loaded;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries); var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 2) { if (subs.Length == 2)
_loaded.Add(new Models.Tag() { {
_loaded.Add(new Models.Tag()
{
Name = subs[0], Name = subs[0],
SHA = subs[1], SHA = subs[1],
}); });
} else if (subs.Length == 3) { }
_loaded.Add(new Models.Tag() { else if (subs.Length == 3)
{
_loaded.Add(new Models.Tag()
{
Name = subs[0], Name = subs[0],
SHA = subs[2], SHA = subs[2],
}); });
} }
} }
private List<Models.Tag> _loaded = new List<Models.Tag>(); private readonly List<Models.Tag> _loaded = new List<Models.Tag>();
} }
} }

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Rebase : Command { {
public Rebase(string repo, string basedOn, bool autoStash) { public class Rebase : Command
{
public Rebase(string repo, string basedOn, bool autoStash)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = "rebase "; Args = "rebase ";

View file

@ -1,31 +1,39 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Remote : Command { {
public Remote(string repo) { public class Remote : Command
{
public Remote(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
} }
public bool Add(string name, string url) { public bool Add(string name, string url)
{
Args = $"remote add {name} {url}"; Args = $"remote add {name} {url}";
return Exec(); return Exec();
} }
public bool Delete(string name) { public bool Delete(string name)
{
Args = $"remote remove {name}"; Args = $"remote remove {name}";
return Exec(); return Exec();
} }
public bool Rename(string name, string to) { public bool Rename(string name, string to)
{
Args = $"remote rename {name} {to}"; Args = $"remote rename {name} {to}";
return Exec(); return Exec();
} }
public bool Prune(string name) { public bool Prune(string name)
{
Args = $"remote prune {name}"; Args = $"remote prune {name}";
return Exec(); return Exec();
} }
public bool SetURL(string name, string url) { public bool SetURL(string name, string url)
{
Args = $"remote set-url {name} {url}"; Args = $"remote set-url {name} {url}";
return Exec(); return Exec();
} }

View file

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

View file

@ -1,9 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Restore : Command { {
public Restore(string repo, List<string> files, string extra) { public class Restore : Command
{
public Restore(string repo, List<string> files, string extra)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;

View file

@ -1,6 +1,9 @@
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Revert : Command { {
public Revert(string repo, string commit, bool autoCommit) { public class Revert : Command
{
public Revert(string repo, string commit, bool autoCommit)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"revert {commit} --no-edit"; Args = $"revert {commit} --no-edit";

View file

@ -1,14 +1,20 @@
using Avalonia.Threading; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
namespace SourceGit.Commands { using Avalonia.Threading;
public static class SaveChangesAsPatch {
public static bool Exec(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo) { namespace SourceGit.Commands
using (var sw = File.Create(saveTo)) { {
foreach (var change in changes) { public static class SaveChangesAsPatch
{
public static bool Exec(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo)
{
using (var sw = File.Create(saveTo))
{
foreach (var change in changes)
{
if (!ProcessSingleChange(repo, new Models.DiffOption(change, isUnstaged), sw)) return false; if (!ProcessSingleChange(repo, new Models.DiffOption(change, isUnstaged), sw)) return false;
} }
} }
@ -16,7 +22,8 @@ namespace SourceGit.Commands {
return true; return true;
} }
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer) { private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
{
var starter = new ProcessStartInfo(); var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo; starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitInstallPath; starter.FileName = Native.OS.GitInstallPath;
@ -26,7 +33,8 @@ namespace SourceGit.Commands {
starter.WindowStyle = ProcessWindowStyle.Hidden; starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardOutput = true; starter.RedirectStandardOutput = true;
try { try
{
var proc = new Process() { StartInfo = starter }; var proc = new Process() { StartInfo = starter };
proc.Start(); proc.Start();
proc.StandardOutput.BaseStream.CopyTo(writer); proc.StandardOutput.BaseStream.CopyTo(writer);
@ -35,8 +43,11 @@ namespace SourceGit.Commands {
proc.Close(); proc.Close();
return rs; return rs;
} catch (Exception e) { }
Dispatcher.UIThread.Invoke(() => { catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Save change to patch failed: " + e.Message); App.RaiseException(repo, "Save change to patch failed: " + e.Message);
}); });
return false; return false;

View file

@ -1,24 +1,33 @@
using Avalonia.Threading; using System;
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
namespace SourceGit.Commands { using Avalonia.Threading;
public static class SaveRevisionFile {
public static void Run(string repo, string revision, string file, string saveTo) { namespace SourceGit.Commands
{
public static class SaveRevisionFile
{
public static void Run(string repo, string revision, string file, string saveTo)
{
var isLFSFiltered = new IsLFSFiltered(repo, file).Result(); var isLFSFiltered = new IsLFSFiltered(repo, file).Result();
if (isLFSFiltered) { if (isLFSFiltered)
{
var tmpFile = saveTo + ".tmp"; var tmpFile = saveTo + ".tmp";
if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) { if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile))
{
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile); ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
} }
File.Delete(tmpFile); File.Delete(tmpFile);
} else { }
else
{
ExecCmd(repo, $"show {revision}:\"{file}\"", saveTo); ExecCmd(repo, $"show {revision}:\"{file}\"", saveTo);
} }
} }
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) { private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
{
var starter = new ProcessStartInfo(); var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo; starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitInstallPath; starter.FileName = Native.OS.GitInstallPath;
@ -30,14 +39,19 @@ namespace SourceGit.Commands {
starter.RedirectStandardOutput = true; starter.RedirectStandardOutput = true;
starter.RedirectStandardError = true; starter.RedirectStandardError = true;
using (var sw = File.OpenWrite(outputFile)) { using (var sw = File.OpenWrite(outputFile))
try { {
try
{
var proc = new Process() { StartInfo = starter }; var proc = new Process() { StartInfo = starter };
proc.Start(); proc.Start();
if (inputFile != null) { if (inputFile != null)
using (StreamReader sr = new StreamReader(inputFile)) { {
while (true) { using (StreamReader sr = new StreamReader(inputFile))
{
while (true)
{
var line = sr.ReadLine(); var line = sr.ReadLine();
if (line == null) break; if (line == null) break;
proc.StandardInput.WriteLine(line); proc.StandardInput.WriteLine(line);
@ -51,8 +65,11 @@ namespace SourceGit.Commands {
proc.Close(); proc.Close();
return rs; return rs;
} catch (Exception e) { }
Dispatcher.UIThread.Invoke(() => { catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Save file failed: " + e.Message); App.RaiseException(repo, "Save file failed: " + e.Message);
}); });
return false; return false;

View file

@ -1,36 +1,45 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Stash : Command { {
public Stash(string repo) { public class Stash : Command
{
public Stash(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
} }
public bool Push(string message) { public bool Push(string message)
{
Args = $"stash push -m \"{message}\""; Args = $"stash push -m \"{message}\"";
return Exec(); return Exec();
} }
public bool Push(List<Models.Change> changes, string message) { public bool Push(List<Models.Change> changes, string message)
{
var temp = Path.GetTempFileName(); var temp = Path.GetTempFileName();
var stream = new FileStream(temp, FileMode.Create); var stream = new FileStream(temp, FileMode.Create);
var writer = new StreamWriter(stream); var writer = new StreamWriter(stream);
var needAdd = new List<Models.Change>(); var needAdd = new List<Models.Change>();
foreach (var c in changes) { foreach (var c in changes)
{
writer.WriteLine(c.Path); writer.WriteLine(c.Path);
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) { if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked)
{
needAdd.Add(c); needAdd.Add(c);
if (needAdd.Count > 10) { if (needAdd.Count > 10)
{
new Add(WorkingDirectory, needAdd).Exec(); new Add(WorkingDirectory, needAdd).Exec();
needAdd.Clear(); needAdd.Clear();
} }
} }
} }
if (needAdd.Count > 0) { if (needAdd.Count > 0)
{
new Add(WorkingDirectory, needAdd).Exec(); new Add(WorkingDirectory, needAdd).Exec();
needAdd.Clear(); needAdd.Clear();
} }
@ -46,22 +55,26 @@ namespace SourceGit.Commands {
return succ; return succ;
} }
public bool Apply(string name) { public bool Apply(string name)
{
Args = $"stash apply -q {name}"; Args = $"stash apply -q {name}";
return Exec(); return Exec();
} }
public bool Pop(string name) { public bool Pop(string name)
{
Args = $"stash pop -q {name}"; Args = $"stash pop -q {name}";
return Exec(); return Exec();
} }
public bool Drop(string name) { public bool Drop(string name)
{
Args = $"stash drop -q {name}"; Args = $"stash drop -q {name}";
return Exec(); return Exec();
} }
public bool Clear() { public bool Clear()
{
Args = "stash clear"; Args = "stash clear";
return Exec(); return Exec();
} }

View file

@ -1,8 +1,11 @@
using System; using System;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Statistics : Command { {
public Statistics(string repo) { public class Statistics : Command
{
public Statistics(string repo)
{
_statistics = new Models.Statistics(); _statistics = new Models.Statistics();
WorkingDirectory = repo; WorkingDirectory = repo;
@ -10,13 +13,15 @@ namespace SourceGit.Commands {
Args = $"log --date-order --branches --remotes --since=\"{_statistics.Since()}\" --pretty=format:\"%ct$%cn\""; Args = $"log --date-order --branches --remotes --since=\"{_statistics.Since()}\" --pretty=format:\"%ct$%cn\"";
} }
public Models.Statistics Result() { public Models.Statistics Result()
{
Exec(); Exec();
_statistics.Complete(); _statistics.Complete();
return _statistics; return _statistics;
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
var dateEndIdx = line.IndexOf('$', StringComparison.Ordinal); var dateEndIdx = line.IndexOf('$', StringComparison.Ordinal);
if (dateEndIdx == -1) return; if (dateEndIdx == -1) return;
@ -27,6 +32,6 @@ namespace SourceGit.Commands {
_statistics.AddCommit(line.Substring(dateEndIdx + 1), date); _statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
} }
private Models.Statistics _statistics = null; private readonly Models.Statistics _statistics = null;
} }
} }

View file

@ -1,32 +1,41 @@
using System; using System;
namespace SourceGit.Commands { namespace SourceGit.Commands
public class Submodule : Command { {
public Submodule(string repo) { public class Submodule : Command
{
public Submodule(string repo)
{
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
} }
public bool Add(string url, string relativePath, bool recursive, Action<string> outputHandler) { public bool Add(string url, string relativePath, bool recursive, Action<string> outputHandler)
{
_outputHandler = outputHandler; _outputHandler = outputHandler;
Args = $"submodule add {url} {relativePath}"; Args = $"submodule add {url} {relativePath}";
if (!Exec()) return false; if (!Exec()) return false;
if (recursive) { if (recursive)
{
Args = $"submodule update --init --recursive -- {relativePath}"; Args = $"submodule update --init --recursive -- {relativePath}";
return Exec(); return Exec();
} else { }
else
{
Args = $"submodule update --init -- {relativePath}"; Args = $"submodule update --init -- {relativePath}";
return true; return true;
} }
} }
public bool Update() { public bool Update()
{
Args = $"submodule update --rebase --remote"; Args = $"submodule update --rebase --remote";
return Exec(); return Exec();
} }
public bool Delete(string relativePath) { public bool Delete(string relativePath)
{
Args = $"submodule deinit -f {relativePath}"; Args = $"submodule deinit -f {relativePath}";
if (!Exec()) return false; if (!Exec()) return false;
@ -34,7 +43,8 @@ namespace SourceGit.Commands {
return Exec(); return Exec();
} }
protected override void OnReadline(string line) { protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line); _outputHandler?.Invoke(line);
} }

View file

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

View file

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

View file

@ -1,8 +1,10 @@
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class BookmarkConverters { {
public static class BookmarkConverters
{
public static FuncValueConverter<int, IBrush> ToBrush = public static FuncValueConverter<int, IBrush> ToBrush =
new FuncValueConverter<int, IBrush>(bookmark => Models.Bookmarks.Brushes[bookmark]); new FuncValueConverter<int, IBrush>(bookmark => Models.Bookmarks.Brushes[bookmark]);

View file

@ -1,7 +1,9 @@
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class BoolConverters { {
public static class BoolConverters
{
public static FuncValueConverter<bool, double> ToCommitOpacity = public static FuncValueConverter<bool, double> ToCommitOpacity =
new FuncValueConverter<bool, double>(x => x ? 1 : 0.5); new FuncValueConverter<bool, double>(x => x ? 1 : 0.5);
} }

View file

@ -1,7 +1,9 @@
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class BranchConverters { {
public static class BranchConverters
{
public static FuncValueConverter<Models.Branch, string> ToName = public static FuncValueConverter<Models.Branch, string> ToName =
new FuncValueConverter<Models.Branch, string>(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}"); new FuncValueConverter<Models.Branch, string>(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}");
} }

View file

@ -2,11 +2,15 @@
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class ChangeViewModeConverters { {
public static class ChangeViewModeConverters
{
public static FuncValueConverter<Models.ChangeViewMode, StreamGeometry> ToIcon = public static FuncValueConverter<Models.ChangeViewMode, StreamGeometry> ToIcon =
new FuncValueConverter<Models.ChangeViewMode, StreamGeometry>(v => { new FuncValueConverter<Models.ChangeViewMode, StreamGeometry>(v =>
switch (v) { {
switch (v)
{
case Models.ChangeViewMode.List: case Models.ChangeViewMode.List:
return App.Current?.FindResource("Icons.List") as StreamGeometry; return App.Current?.FindResource("Icons.List") as StreamGeometry;
case Models.ChangeViewMode.Grid: case Models.ChangeViewMode.Grid:

View file

@ -3,18 +3,23 @@ using Avalonia.Controls;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class DecoratorTypeConverters { {
public static class DecoratorTypeConverters
{
public static FuncValueConverter<Models.DecoratorType, IBrush> ToBackground = public static FuncValueConverter<Models.DecoratorType, IBrush> ToBackground =
new FuncValueConverter<Models.DecoratorType, IBrush>(v => { new FuncValueConverter<Models.DecoratorType, IBrush>(v =>
{
if (v == Models.DecoratorType.Tag) return Models.DecoratorResources.Backgrounds[0]; if (v == Models.DecoratorType.Tag) return Models.DecoratorResources.Backgrounds[0];
return Models.DecoratorResources.Backgrounds[1]; return Models.DecoratorResources.Backgrounds[1];
}); });
public static FuncValueConverter<Models.DecoratorType, StreamGeometry> ToIcon = public static FuncValueConverter<Models.DecoratorType, StreamGeometry> ToIcon =
new FuncValueConverter<Models.DecoratorType, StreamGeometry>(v => { new FuncValueConverter<Models.DecoratorType, StreamGeometry>(v =>
{
var key = "Icons.Tag"; var key = "Icons.Tag";
switch (v) { switch (v)
{
case Models.DecoratorType.CurrentBranchHead: case Models.DecoratorType.CurrentBranchHead:
key = "Icons.Check"; key = "Icons.Check";
break; break;

View file

@ -1,7 +1,9 @@
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class IntConverters { {
public static class IntConverters
{
public static FuncValueConverter<int, bool> IsGreaterThanZero = public static FuncValueConverter<int, bool> IsGreaterThanZero =
new FuncValueConverter<int, bool>(v => v > 0); new FuncValueConverter<int, bool>(v => v > 0);

View file

@ -1,11 +1,15 @@
using Avalonia.Collections; using System.Collections.Generic;
using Avalonia.Data.Converters;
using System.Collections.Generic;
namespace SourceGit.Converters { using Avalonia.Collections;
public static class LauncherPageConverters { using Avalonia.Data.Converters;
namespace SourceGit.Converters
{
public static class LauncherPageConverters
{
public static FuncMultiValueConverter<object, bool> ToTabSeperatorVisible = public static FuncMultiValueConverter<object, bool> ToTabSeperatorVisible =
new FuncMultiValueConverter<object, bool>(v => { new FuncMultiValueConverter<object, bool>(v =>
{
if (v == null) return false; if (v == null) return false;
var array = new List<object>(); var array = new List<object>();
@ -18,9 +22,12 @@ namespace SourceGit.Converters {
var selected = array[1] as ViewModels.LauncherPage; var selected = array[1] as ViewModels.LauncherPage;
var collections = array[2] as AvaloniaList<ViewModels.LauncherPage>; var collections = array[2] as AvaloniaList<ViewModels.LauncherPage>;
if (selected != null && collections != null && (self == selected || collections.IndexOf(self) + 1 == collections.IndexOf(selected))) { if (selected != null && collections != null && (self == selected || collections.IndexOf(self) + 1 == collections.IndexOf(selected)))
{
return false; return false;
} else { }
else
{
return true; return true;
} }
}); });

View file

@ -1,8 +1,11 @@
using Avalonia.Data.Converters; using System.Collections;
using System.Collections;
namespace SourceGit.Converters { using Avalonia.Data.Converters;
public static class ListConverters {
namespace SourceGit.Converters
{
public static class ListConverters
{
public static FuncValueConverter<IList, string> ToCount = public static FuncValueConverter<IList, string> ToCount =
new FuncValueConverter<IList, string>(v => $" ({v.Count})"); new FuncValueConverter<IList, string>(v => $" ({v.Count})");

View file

@ -1,8 +1,11 @@
using Avalonia.Data.Converters; using System.IO;
using System.IO;
namespace SourceGit.Converters { using Avalonia.Data.Converters;
public static class PathConverters {
namespace SourceGit.Converters
{
public static class PathConverters
{
public static FuncValueConverter<string, string> PureFileName = public static FuncValueConverter<string, string> PureFileName =
new FuncValueConverter<string, string>(fullpath => Path.GetFileName(fullpath) ?? ""); new FuncValueConverter<string, string>(fullpath => Path.GetFileName(fullpath) ?? "");
@ -10,7 +13,8 @@ namespace SourceGit.Converters {
new FuncValueConverter<string, string>(fullpath => Path.GetDirectoryName(fullpath) ?? ""); new FuncValueConverter<string, string>(fullpath => Path.GetDirectoryName(fullpath) ?? "");
public static FuncValueConverter<string, string> TruncateIfTooLong = public static FuncValueConverter<string, string> TruncateIfTooLong =
new FuncValueConverter<string, string>(fullpath => { new FuncValueConverter<string, string>(fullpath =>
{
if (fullpath.Length <= 50) return fullpath; if (fullpath.Length <= 50) return fullpath;
return fullpath.Substring(0, 20) + ".../" + Path.GetFileName(fullpath); return fullpath.Substring(0, 20) + ".../" + Path.GetFileName(fullpath);
}); });

View file

@ -1,35 +1,49 @@
using Avalonia.Data.Converters; using System;
using Avalonia.Styling;
using System;
using System.Globalization; using System.Globalization;
namespace SourceGit.Converters { using Avalonia.Data.Converters;
public static class StringConverters { using Avalonia.Styling;
public class ToLocaleConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { namespace SourceGit.Converters
{
public static class StringConverters
{
public class ToLocaleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Models.Locale.Supported.Find(x => x.Key == value as string); return Models.Locale.Supported.Find(x => x.Key == value as string);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value as Models.Locale).Key; return (value as Models.Locale).Key;
} }
} }
public static ToLocaleConverter ToLocale = new ToLocaleConverter(); public static ToLocaleConverter ToLocale = new ToLocaleConverter();
public class ToThemeConverter : IValueConverter { public class ToThemeConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var theme = (string)value; var theme = (string)value;
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) { if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
{
return ThemeVariant.Light; return ThemeVariant.Light;
} else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) { }
else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase))
{
return ThemeVariant.Dark; return ThemeVariant.Dark;
} else { }
else
{
return ThemeVariant.Default; return ThemeVariant.Default;
} }
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var theme = (ThemeVariant)value; var theme = (ThemeVariant)value;
return theme.Key; return theme.Key;
} }
@ -37,13 +51,16 @@ namespace SourceGit.Converters {
public static ToThemeConverter ToTheme = new ToThemeConverter(); public static ToThemeConverter ToTheme = new ToThemeConverter();
public class FormatByResourceKeyConverter : IValueConverter { public class FormatByResourceKeyConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var key = parameter as string; var key = parameter as string;
return App.Text(key, value); return App.Text(key, value);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }

View file

@ -1,34 +1,53 @@
using Avalonia.Controls; using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
using Avalonia;
using System.Runtime.InteropServices;
namespace SourceGit.Converters { namespace SourceGit.Converters
public static class WindowStateConverters { {
public static class WindowStateConverters
{
public static FuncValueConverter<WindowState, Thickness> ToContentMargin = public static FuncValueConverter<WindowState, Thickness> ToContentMargin =
new FuncValueConverter<WindowState, Thickness>(state => { new FuncValueConverter<WindowState, Thickness>(state =>
if (state == WindowState.Maximized && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { {
if (OperatingSystem.IsWindows() && state == WindowState.Maximized)
{
return new Thickness(6); return new Thickness(6);
} else { }
else if (OperatingSystem.IsLinux() && state != WindowState.Maximized)
{
return new Thickness(6);
}
else
{
return new Thickness(0); return new Thickness(0);
} }
}); });
public static FuncValueConverter<WindowState, GridLength> ToTitleBarHeight = public static FuncValueConverter<WindowState, GridLength> ToTitleBarHeight =
new FuncValueConverter<WindowState, GridLength>(state => { new FuncValueConverter<WindowState, GridLength>(state =>
if (state == WindowState.Maximized && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { {
if (state == WindowState.Maximized)
{
return new GridLength(30); return new GridLength(30);
} else { }
else
{
return new GridLength(38); return new GridLength(38);
} }
}); });
public static FuncValueConverter<WindowState, StreamGeometry> ToMaxOrRestoreIcon = public static FuncValueConverter<WindowState, StreamGeometry> ToMaxOrRestoreIcon =
new FuncValueConverter<WindowState, StreamGeometry>(state => { new FuncValueConverter<WindowState, StreamGeometry>(state =>
if (state == WindowState.Maximized) { {
if (state == WindowState.Maximized)
{
return Application.Current?.FindResource("Icons.Window.Restore") as StreamGeometry; return Application.Current?.FindResource("Icons.Window.Restore") as StreamGeometry;
} else { }
else
{
return Application.Current?.FindResource("Icons.Window.Maximize") as StreamGeometry; return Application.Current?.FindResource("Icons.Window.Maximize") as StreamGeometry;
} }
}); });

View file

@ -1,10 +1,13 @@
namespace SourceGit.Models { namespace SourceGit.Models
public class ApplyWhiteSpaceMode { {
public class ApplyWhiteSpaceMode
{
public string Name { get; set; } public string Name { get; set; }
public string Desc { get; set; } public string Desc { get; set; }
public string Arg { get; set; } public string Arg { get; set; }
public ApplyWhiteSpaceMode(string n, string d, string a) { public ApplyWhiteSpaceMode(string n, string d, string a)
{
Name = App.Text(n); Name = App.Text(n);
Desc = App.Text(d); Desc = App.Text(d);
Arg = a; Arg = a;

View file

@ -1,69 +1,88 @@
using Avalonia.Media.Imaging; using System;
using Avalonia.Threading;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SourceGit.Models { using Avalonia.Media.Imaging;
public interface IAvatarHost { using Avalonia.Threading;
namespace SourceGit.Models
{
public interface IAvatarHost
{
void OnAvatarResourceChanged(string md5); void OnAvatarResourceChanged(string md5);
} }
public static class AvatarManager { public static class AvatarManager
public static string SelectedServer { {
public static string SelectedServer
{
get; get;
set; set;
} = "https://www.gravatar.com/avatar/"; } = "https://www.gravatar.com/avatar/";
static AvatarManager() { static AvatarManager()
{
_storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit", "avatars"); _storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit", "avatars");
if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath); if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath);
Task.Run(() => { Task.Run(() =>
while (true) { {
while (true)
{
var md5 = null as string; var md5 = null as string;
lock (_synclock) { lock (_synclock)
foreach (var one in _requesting) { {
foreach (var one in _requesting)
{
md5 = one; md5 = one;
break; break;
} }
} }
if (md5 == null) { if (md5 == null)
{
Thread.Sleep(100); Thread.Sleep(100);
continue; continue;
} }
var localFile = Path.Combine(_storePath, md5); var localFile = Path.Combine(_storePath, md5);
var img = null as Bitmap; var img = null as Bitmap;
try { try
{
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) }; var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) };
var task = client.GetAsync($"{SelectedServer}{md5}?d=404"); var task = client.GetAsync($"{SelectedServer}{md5}?d=404");
task.Wait(); task.Wait();
var rsp = task.Result; var rsp = task.Result;
if (rsp.IsSuccessStatusCode) { if (rsp.IsSuccessStatusCode)
using (var stream = rsp.Content.ReadAsStream()) { {
using (var writer = File.OpenWrite(localFile)) { using (var stream = rsp.Content.ReadAsStream())
{
using (var writer = File.OpenWrite(localFile))
{
stream.CopyTo(writer); stream.CopyTo(writer);
} }
} }
using (var reader = File.OpenRead(localFile)) { using (var reader = File.OpenRead(localFile))
{
img = Bitmap.DecodeToWidth(reader, 128); img = Bitmap.DecodeToWidth(reader, 128);
} }
} }
} catch { } }
catch { }
lock (_synclock) { lock (_synclock)
{
_requesting.Remove(md5); _requesting.Remove(md5);
} }
Dispatcher.UIThread.InvokeAsync(() => { Dispatcher.UIThread.InvokeAsync(() =>
{
if (_resources.ContainsKey(md5)) _resources[md5] = img; if (_resources.ContainsKey(md5)) _resources[md5] = img;
else _resources.Add(md5, img); else _resources.Add(md5, img);
NotifyResourceChanged(md5); NotifyResourceChanged(md5);
@ -72,54 +91,67 @@ namespace SourceGit.Models {
}); });
} }
public static void Subscribe(IAvatarHost host) { public static void Subscribe(IAvatarHost host)
{
_avatars.Add(host); _avatars.Add(host);
} }
public static void Unsubscribe(IAvatarHost host) { public static void Unsubscribe(IAvatarHost host)
{
_avatars.Remove(host); _avatars.Remove(host);
} }
public static Bitmap Request(string md5, bool forceRefetch = false) { public static Bitmap Request(string md5, bool forceRefetch = false)
if (forceRefetch) { {
if (forceRefetch)
{
if (_resources.ContainsKey(md5)) _resources.Remove(md5); if (_resources.ContainsKey(md5)) _resources.Remove(md5);
var localFile = Path.Combine(_storePath, md5); var localFile = Path.Combine(_storePath, md5);
if (File.Exists(localFile)) File.Delete(localFile); if (File.Exists(localFile)) File.Delete(localFile);
NotifyResourceChanged(md5); NotifyResourceChanged(md5);
} else { }
else
{
if (_resources.ContainsKey(md5)) return _resources[md5]; if (_resources.ContainsKey(md5)) return _resources[md5];
var localFile = Path.Combine(_storePath, md5); var localFile = Path.Combine(_storePath, md5);
if (File.Exists(localFile)) { if (File.Exists(localFile))
try { {
using (var stream = File.OpenRead(localFile)) { try
{
using (var stream = File.OpenRead(localFile))
{
var img = Bitmap.DecodeToWidth(stream, 128); var img = Bitmap.DecodeToWidth(stream, 128);
_resources.Add(md5, img); _resources.Add(md5, img);
return img; return img;
} }
} catch { } }
catch { }
} }
} }
lock (_synclock) { lock (_synclock)
{
if (!_requesting.Contains(md5)) _requesting.Add(md5); if (!_requesting.Contains(md5)) _requesting.Add(md5);
} }
return null; return null;
} }
private static void NotifyResourceChanged(string md5) { private static void NotifyResourceChanged(string md5)
foreach (var avatar in _avatars) { {
foreach (var avatar in _avatars)
{
avatar.OnAvatarResourceChanged(md5); avatar.OnAvatarResourceChanged(md5);
} }
} }
private static object _synclock = new object(); private static readonly object _synclock = new object();
private static string _storePath = string.Empty; private static readonly string _storePath = string.Empty;
private static List<IAvatarHost> _avatars = new List<IAvatarHost>(); private static readonly List<IAvatarHost> _avatars = new List<IAvatarHost>();
private static Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>(); private static readonly Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
private static HashSet<string> _requesting = new HashSet<string>(); private static readonly HashSet<string> _requesting = new HashSet<string>();
} }
} }

View file

@ -1,14 +1,17 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class BlameLineInfo { {
public class BlameLineInfo
{
public bool IsFirstInGroup { get; set; } = false; public bool IsFirstInGroup { get; set; } = false;
public string CommitSHA { get; set; } = string.Empty; public string CommitSHA { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty; public string Author { get; set; } = string.Empty;
public string Time { get; set; } = string.Empty; public string Time { get; set; } = string.Empty;
} }
public class BlameData { public class BlameData
{
public string File { get; set; } = string.Empty; public string File { get; set; } = string.Empty;
public List<BlameLineInfo> LineInfos { get; set; } = new List<BlameLineInfo>(); public List<BlameLineInfo> LineInfos { get; set; } = new List<BlameLineInfo>();
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;

View file

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public static class Bookmarks { {
public static class Bookmarks
{
public static readonly Avalonia.Media.IBrush[] Brushes = [ public static readonly Avalonia.Media.IBrush[] Brushes = [
Avalonia.Media.Brushes.Transparent, Avalonia.Media.Brushes.Transparent,
Avalonia.Media.Brushes.Red, Avalonia.Media.Brushes.Red,
@ -15,7 +17,8 @@ namespace SourceGit.Models {
public static readonly List<int> Supported = new List<int>(); public static readonly List<int> Supported = new List<int>();
static Bookmarks() { static Bookmarks()
{
for (int i = 0; i < Brushes.Length; i++) Supported.Add(i); for (int i = 0; i < Brushes.Length; i++) Supported.Add(i);
} }
} }

View file

@ -1,5 +1,7 @@
namespace SourceGit.Models { namespace SourceGit.Models
public class Branch { {
public class Branch
{
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; set; } public string FullName { get; set; }
public string Head { get; set; } public string Head { get; set; }

View file

@ -1,15 +1,19 @@
using Avalonia.Collections; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { using Avalonia.Collections;
public enum BranchTreeNodeType {
namespace SourceGit.Models
{
public enum BranchTreeNodeType
{
Remote, Remote,
Folder, Folder,
Branch, Branch,
} }
public class BranchTreeNode { public class BranchTreeNode
{
public string Name { get; set; } public string Name { get; set; }
public BranchTreeNodeType Type { get; set; } public BranchTreeNodeType Type { get; set; }
public object Backend { get; set; } public object Backend { get; set; }
@ -17,38 +21,48 @@ namespace SourceGit.Models {
public bool IsFiltered { get; set; } public bool IsFiltered { get; set; }
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>(); public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
public bool IsUpstreamTrackStatusVisible { public bool IsUpstreamTrackStatusVisible
{
get => IsBranch && !string.IsNullOrEmpty((Backend as Branch).UpstreamTrackStatus); get => IsBranch && !string.IsNullOrEmpty((Backend as Branch).UpstreamTrackStatus);
} }
public string UpstreamTrackStatus { public string UpstreamTrackStatus
{
get => Type == BranchTreeNodeType.Branch ? (Backend as Branch).UpstreamTrackStatus : ""; get => Type == BranchTreeNodeType.Branch ? (Backend as Branch).UpstreamTrackStatus : "";
} }
public bool IsRemote { public bool IsRemote
{
get => Type == BranchTreeNodeType.Remote; get => Type == BranchTreeNodeType.Remote;
} }
public bool IsFolder { public bool IsFolder
{
get => Type == BranchTreeNodeType.Folder; get => Type == BranchTreeNodeType.Folder;
} }
public bool IsBranch { public bool IsBranch
{
get => Type == BranchTreeNodeType.Branch; get => Type == BranchTreeNodeType.Branch;
} }
public bool IsCurrent { public bool IsCurrent
{
get => IsBranch && (Backend as Branch).IsCurrent; get => IsBranch && (Backend as Branch).IsCurrent;
} }
public class Builder { public class Builder
{
public List<BranchTreeNode> Locals => _locals; public List<BranchTreeNode> Locals => _locals;
public List<BranchTreeNode> Remotes => _remotes; public List<BranchTreeNode> Remotes => _remotes;
public void Run(List<Branch> branches, List<Remote> remotes) { public void Run(List<Branch> branches, List<Remote> remotes)
foreach (var remote in remotes) { {
foreach (var remote in remotes)
{
var path = $"remote/{remote.Name}"; var path = $"remote/{remote.Name}";
var node = new BranchTreeNode() { var node = new BranchTreeNode()
{
Name = remote.Name, Name = remote.Name,
Type = BranchTreeNodeType.Remote, Type = BranchTreeNodeType.Remote,
Backend = remote, Backend = remote,
@ -59,11 +73,15 @@ namespace SourceGit.Models {
_remotes.Add(node); _remotes.Add(node);
} }
foreach (var branch in branches) { foreach (var branch in branches)
{
var isFiltered = _filters.Contains(branch.FullName); var isFiltered = _filters.Contains(branch.FullName);
if (branch.IsLocal) { if (branch.IsLocal)
{
MakeBranchNode(branch, _locals, "local", isFiltered); MakeBranchNode(branch, _locals, "local", isFiltered);
} else { }
else
{
var remote = _remotes.Find(x => x.Name == branch.Remote); var remote = _remotes.Find(x => x.Name == branch.Remote);
if (remote != null) MakeBranchNode(branch, remote.Children, $"remote/{remote.Name}", isFiltered); if (remote != null) MakeBranchNode(branch, remote.Children, $"remote/{remote.Name}", isFiltered);
} }
@ -73,27 +91,34 @@ namespace SourceGit.Models {
SortNodes(_remotes); SortNodes(_remotes);
} }
public void SetFilters(AvaloniaList<string> filters) { public void SetFilters(AvaloniaList<string> filters)
{
_filters.AddRange(filters); _filters.AddRange(filters);
} }
public void CollectExpandedNodes(List<BranchTreeNode> nodes, bool isLocal) { public void CollectExpandedNodes(List<BranchTreeNode> nodes, bool isLocal)
{
CollectExpandedNodes(nodes, isLocal ? "local" : "remote"); CollectExpandedNodes(nodes, isLocal ? "local" : "remote");
} }
private void CollectExpandedNodes(List<BranchTreeNode> nodes, string prefix) { private void CollectExpandedNodes(List<BranchTreeNode> nodes, string prefix)
foreach (var node in nodes) { {
foreach (var node in nodes)
{
var path = prefix + "/" + node.Name; var path = prefix + "/" + node.Name;
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) _expanded.Add(path); if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) _expanded.Add(path);
CollectExpandedNodes(node.Children, path); CollectExpandedNodes(node.Children, path);
} }
} }
private void MakeBranchNode(Branch branch, List<BranchTreeNode> roots, string prefix, bool isFiltered) { private void MakeBranchNode(Branch branch, List<BranchTreeNode> roots, string prefix, bool isFiltered)
{
var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 1) { if (subs.Length == 1)
var node = new BranchTreeNode() { {
var node = new BranchTreeNode()
{
Name = subs[0], Name = subs[0],
Type = BranchTreeNodeType.Branch, Type = BranchTreeNodeType.Branch,
Backend = branch, Backend = branch,
@ -106,20 +131,28 @@ namespace SourceGit.Models {
BranchTreeNode lastFolder = null; BranchTreeNode lastFolder = null;
string path = prefix; string path = prefix;
for (int i = 0; i < subs.Length - 1; i++) { for (int i = 0; i < subs.Length - 1; i++)
{
path = string.Concat(path, "/", subs[i]); path = string.Concat(path, "/", subs[i]);
if (_maps.ContainsKey(path)) { if (_maps.ContainsKey(path))
{
lastFolder = _maps[path]; lastFolder = _maps[path];
} else if (lastFolder == null) { }
lastFolder = new BranchTreeNode() { else if (lastFolder == null)
{
lastFolder = new BranchTreeNode()
{
Name = subs[i], Name = subs[i],
Type = BranchTreeNodeType.Folder, Type = BranchTreeNodeType.Folder,
IsExpanded = branch.IsCurrent || _expanded.Contains(path), IsExpanded = branch.IsCurrent || _expanded.Contains(path),
}; };
roots.Add(lastFolder); roots.Add(lastFolder);
_maps.Add(path, lastFolder); _maps.Add(path, lastFolder);
} else { }
var folder = new BranchTreeNode() { else
{
var folder = new BranchTreeNode()
{
Name = subs[i], Name = subs[i],
Type = BranchTreeNodeType.Folder, Type = BranchTreeNodeType.Folder,
IsExpanded = branch.IsCurrent || _expanded.Contains(path), IsExpanded = branch.IsCurrent || _expanded.Contains(path),
@ -130,7 +163,8 @@ namespace SourceGit.Models {
} }
} }
var last = new BranchTreeNode() { var last = new BranchTreeNode()
{
Name = subs[subs.Length - 1], Name = subs[subs.Length - 1],
Type = BranchTreeNodeType.Branch, Type = BranchTreeNodeType.Branch,
Backend = branch, Backend = branch,
@ -140,11 +174,16 @@ namespace SourceGit.Models {
lastFolder.Children.Add(last); lastFolder.Children.Add(last);
} }
private void SortNodes(List<BranchTreeNode> nodes) { private void SortNodes(List<BranchTreeNode> nodes)
nodes.Sort((l, r) => { {
if (l.Type == r.Type) { nodes.Sort((l, r) =>
{
if (l.Type == r.Type)
{
return l.Name.CompareTo(r.Name); return l.Name.CompareTo(r.Name);
} else { }
else
{
return (int)(l.Type) - (int)(r.Type); return (int)(l.Type) - (int)(r.Type);
} }
}); });
@ -152,11 +191,11 @@ namespace SourceGit.Models {
foreach (var node in nodes) SortNodes(node.Children); foreach (var node in nodes) SortNodes(node.Children);
} }
private List<BranchTreeNode> _locals = new List<BranchTreeNode>(); private readonly List<BranchTreeNode> _locals = new List<BranchTreeNode>();
private List<BranchTreeNode> _remotes = new List<BranchTreeNode>(); private readonly List<BranchTreeNode> _remotes = new List<BranchTreeNode>();
private HashSet<string> _expanded = new HashSet<string>(); private readonly HashSet<string> _expanded = new HashSet<string>();
private List<string> _filters = new List<string>(); private readonly List<string> _filters = new List<string>();
private Dictionary<string, BranchTreeNode> _maps = new Dictionary<string, BranchTreeNode>(); private readonly Dictionary<string, BranchTreeNode> _maps = new Dictionary<string, BranchTreeNode>();
} }
} }
} }

View file

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class CRLFMode { {
public class CRLFMode
{
public string Name { get; set; } public string Name { get; set; }
public string Value { get; set; } public string Value { get; set; }
public string Desc { get; set; } public string Desc { get; set; }
@ -12,7 +14,8 @@ namespace SourceGit.Models {
new CRLFMode("FALSE", "false", "Do NOT convert"), new CRLFMode("FALSE", "false", "Do NOT convert"),
}; };
public CRLFMode(string name, string value, string desc) { public CRLFMode(string name, string value, string desc)
{
Name = name; Name = name;
Value = value; Value = value;
Desc = desc; Desc = desc;

View file

@ -1,11 +1,16 @@
namespace SourceGit.Models { using System;
public enum ChangeViewMode {
namespace SourceGit.Models
{
public enum ChangeViewMode
{
List, List,
Grid, Grid,
Tree, Tree,
} }
public enum ChangeState { public enum ChangeState
{
None, None,
Modified, Modified,
Added, Added,
@ -16,14 +21,17 @@
Untracked Untracked
} }
public class Change { public class Change
{
public ChangeState Index { get; set; } public ChangeState Index { get; set; }
public ChangeState WorkTree { get; set; } = ChangeState.None; public ChangeState WorkTree { get; set; } = ChangeState.None;
public string Path { get; set; } = ""; public string Path { get; set; } = "";
public string OriginalPath { get; set; } = ""; public string OriginalPath { get; set; } = "";
public bool IsConflit { public bool IsConflit
get { {
get
{
if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged) return true; if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged) return true;
if (Index == ChangeState.Added && WorkTree == ChangeState.Added) return true; if (Index == ChangeState.Added && WorkTree == ChangeState.Added) return true;
if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted) return true; if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted) return true;
@ -31,18 +39,24 @@
} }
} }
public void Set(ChangeState index, ChangeState workTree = ChangeState.None) { public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{
Index = index; Index = index;
WorkTree = workTree; WorkTree = workTree;
if (index == ChangeState.Renamed || workTree == ChangeState.Renamed) { if (index == ChangeState.Renamed || workTree == ChangeState.Renamed)
var idx = Path.IndexOf('\t'); {
if (idx >= 0) { var idx = Path.IndexOf('\t', StringComparison.Ordinal);
if (idx >= 0)
{
OriginalPath = Path.Substring(0, idx); OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 1); Path = Path.Substring(idx + 1);
} else { }
idx = Path.IndexOf(" -> "); else
if (idx > 0) { {
idx = Path.IndexOf(" -> ", StringComparison.Ordinal);
if (idx > 0)
{
OriginalPath = Path.Substring(0, idx); OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 4); Path = Path.Substring(idx + 4);
} }

View file

@ -1,9 +1,12 @@
using Avalonia; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { using Avalonia;
public class Commit {
namespace SourceGit.Models
{
public class Commit
{
public string SHA { get; set; } = string.Empty; public string SHA { get; set; } = string.Empty;
public User Author { get; set; } = User.Invalid; public User Author { get; set; } = User.Invalid;
public ulong AuthorTime { get; set; } = 0; public ulong AuthorTime { get; set; } = 0;
@ -22,16 +25,19 @@ namespace SourceGit.Models {
public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd"); public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
public bool IsCommitterVisible { public bool IsCommitterVisible
{
get => Author != Committer || AuthorTime != CommitterTime; get => Author != Committer || AuthorTime != CommitterTime;
} }
public string FullMessage { public string FullMessage
{
get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}"; get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}";
} }
public static void ParseUserAndTime(string data, ref User user, ref ulong time) { public static void ParseUserAndTime(string data, ref User user, ref ulong time)
var userEndIdx = data.IndexOf('>'); {
var userEndIdx = data.IndexOf('>', StringComparison.Ordinal);
if (userEndIdx < 0) return; if (userEndIdx < 0) return;
var timeEndIdx = data.IndexOf(' ', userEndIdx + 2); var timeEndIdx = data.IndexOf(' ', userEndIdx + 2);

View file

@ -1,15 +1,20 @@
using Avalonia; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { using Avalonia;
public class CommitGraph {
public class Path { namespace SourceGit.Models
{
public class CommitGraph
{
public class Path
{
public List<Point> Points = new List<Point>(); public List<Point> Points = new List<Point>();
public int Color = 0; public int Color = 0;
} }
public class PathHelper { public class PathHelper
{
public string Next; public string Next;
public bool IsMerged; public bool IsMerged;
public double LastX; public double LastX;
@ -17,7 +22,8 @@ namespace SourceGit.Models {
public double EndY; public double EndY;
public Path Path; public Path Path;
public PathHelper(string next, bool isMerged, int color, Point start) { public PathHelper(string next, bool isMerged, int color, Point start)
{
Next = next; Next = next;
IsMerged = isMerged; IsMerged = isMerged;
LastX = start.X; LastX = start.X;
@ -29,7 +35,8 @@ namespace SourceGit.Models {
Path.Points.Add(start); Path.Points.Add(start);
} }
public PathHelper(string next, bool isMerged, int color, Point start, Point to) { public PathHelper(string next, bool isMerged, int color, Point start, Point to)
{
Next = next; Next = next;
IsMerged = isMerged; IsMerged = isMerged;
LastX = to.X; LastX = to.X;
@ -42,15 +49,21 @@ namespace SourceGit.Models {
Path.Points.Add(to); Path.Points.Add(to);
} }
public void Add(double x, double y, double halfHeight, bool isEnd = false) { public void Add(double x, double y, double halfHeight, bool isEnd = false)
if (x > LastX) { {
if (x > LastX)
{
Add(new Point(LastX, LastY)); Add(new Point(LastX, LastY));
Add(new Point(x, y - halfHeight)); Add(new Point(x, y - halfHeight));
if (isEnd) Add(new Point(x, y)); if (isEnd) Add(new Point(x, y));
} else if (x < LastX) { }
else if (x < LastX)
{
if (y > LastY + halfHeight) Add(new Point(LastX, LastY + halfHeight)); if (y > LastY + halfHeight) Add(new Point(LastX, LastY + halfHeight));
Add(new Point(x, y)); Add(new Point(x, y));
} else if (isEnd) { }
else if (isEnd)
{
Add(new Point(x, y)); Add(new Point(x, y));
} }
@ -58,22 +71,26 @@ namespace SourceGit.Models {
LastY = y; LastY = y;
} }
private void Add(Point p) { private void Add(Point p)
if (EndY < p.Y) { {
if (EndY < p.Y)
{
Path.Points.Add(p); Path.Points.Add(p);
EndY = p.Y; EndY = p.Y;
} }
} }
} }
public class Link { public class Link
{
public Point Start; public Point Start;
public Point Control; public Point Control;
public Point End; public Point End;
public int Color; public int Color;
} }
public class Dot { public class Dot
{
public Point Center; public Point Center;
public int Color; public int Color;
} }
@ -82,7 +99,8 @@ namespace SourceGit.Models {
public List<Link> Links { get; set; } = new List<Link>(); public List<Link> Links { get; set; } = new List<Link>();
public List<Dot> Dots { get; set; } = new List<Dot>(); public List<Dot> Dots { get; set; } = new List<Dot>();
public static CommitGraph Parse(List<Commit> commits, double rowHeight, int colorCount) { public static CommitGraph Parse(List<Commit> commits, double rowHeight, int colorCount)
{
double UNIT_WIDTH = 12; double UNIT_WIDTH = 12;
double HALF_WIDTH = 6; double HALF_WIDTH = 6;
double UNIT_HEIGHT = rowHeight; double UNIT_HEIGHT = rowHeight;
@ -95,7 +113,8 @@ namespace SourceGit.Models {
var offsetY = -HALF_HEIGHT; var offsetY = -HALF_HEIGHT;
var colorIdx = 0; var colorIdx = 0;
foreach (var commit in commits) { foreach (var commit in commits)
{
var major = null as PathHelper; var major = null as PathHelper;
var isMerged = commit.IsMerged; var isMerged = commit.IsMerged;
var oldCount = unsolved.Count; var oldCount = unsolved.Count;
@ -105,27 +124,37 @@ namespace SourceGit.Models {
// Find first curves that links to this commit and marks others that links to this commit ended. // Find first curves that links to this commit and marks others that links to this commit ended.
double offsetX = -HALF_WIDTH; double offsetX = -HALF_WIDTH;
foreach (var l in unsolved) { foreach (var l in unsolved)
if (l.Next == commit.SHA) { {
if (major == null) { if (l.Next == commit.SHA)
{
if (major == null)
{
offsetX += UNIT_WIDTH; offsetX += UNIT_WIDTH;
major = l; major = l;
if (commit.Parents.Count > 0) { if (commit.Parents.Count > 0)
{
major.Next = commit.Parents[0]; major.Next = commit.Parents[0];
if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major); if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major);
} else { }
else
{
major.Next = "ENDED"; major.Next = "ENDED";
ended.Add(l); ended.Add(l);
} }
major.Add(offsetX, offsetY, HALF_HEIGHT); major.Add(offsetX, offsetY, HALF_HEIGHT);
} else { }
else
{
ended.Add(l); ended.Add(l);
} }
isMerged = isMerged || l.IsMerged; isMerged = isMerged || l.IsMerged;
} else { }
else
{
if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l); if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l);
offsetX += UNIT_WIDTH; offsetX += UNIT_WIDTH;
l.Add(offsetX, offsetY, HALF_HEIGHT); l.Add(offsetX, offsetY, HALF_HEIGHT);
@ -133,7 +162,8 @@ namespace SourceGit.Models {
} }
// Create new curve for branch head // Create new curve for branch head
if (major == null && commit.Parents.Count > 0) { if (major == null && commit.Parents.Count > 0)
{
offsetX += UNIT_WIDTH; offsetX += UNIT_WIDTH;
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major); unsolved.Add(major);
@ -143,18 +173,23 @@ namespace SourceGit.Models {
// Calculate link position of this commit. // Calculate link position of this commit.
Point position = new Point(offsetX, offsetY); Point position = new Point(offsetX, offsetY);
if (major != null) { if (major != null)
{
major.IsMerged = isMerged; major.IsMerged = isMerged;
position = new Point(major.LastX, offsetY); position = new Point(major.LastX, offsetY);
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color }); temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color });
} else { }
else
{
temp.Dots.Add(new Dot() { Center = position, Color = 0 }); temp.Dots.Add(new Dot() { Center = position, Color = 0 });
} }
// Deal with parents // Deal with parents
for (int j = 1; j < commit.Parents.Count; j++) { for (int j = 1; j < commit.Parents.Count; j++)
{
var parent = commit.Parents[j]; var parent = commit.Parents[j];
if (mapUnsolved.ContainsKey(parent)) { if (mapUnsolved.ContainsKey(parent))
{
var l = mapUnsolved[parent]; var l = mapUnsolved[parent];
var link = new Link(); var link = new Link();
@ -163,7 +198,9 @@ namespace SourceGit.Models {
link.Control = new Point(link.End.X, link.Start.Y); link.Control = new Point(link.End.X, link.Start.Y);
link.Color = l.Path.Color; link.Color = l.Path.Color;
temp.Links.Add(link); temp.Links.Add(link);
} else { }
else
{
offsetX += UNIT_WIDTH; offsetX += UNIT_WIDTH;
// Create new curve for parent commit that not includes before // Create new curve for parent commit that not includes before
@ -175,7 +212,8 @@ namespace SourceGit.Models {
} }
// Remove ended curves from unsolved // Remove ended curves from unsolved
foreach (var l in ended) { foreach (var l in ended)
{
l.Add(position.X, position.Y, HALF_HEIGHT, true); l.Add(position.X, position.Y, HALF_HEIGHT, true);
unsolved.Remove(l); unsolved.Remove(l);
} }
@ -190,7 +228,8 @@ namespace SourceGit.Models {
} }
// Deal with curves haven't ended yet. // Deal with curves haven't ended yet.
for (int i = 0; i < unsolved.Count; i++) { for (int i = 0; i < unsolved.Count; i++)
{
var path = unsolved[i]; var path = unsolved[i];
var endY = (commits.Count - 0.5) * UNIT_HEIGHT; var endY = (commits.Count - 0.5) * UNIT_HEIGHT;

View file

@ -1,7 +1,9 @@
using Avalonia.Media; using Avalonia.Media;
namespace SourceGit.Models { namespace SourceGit.Models
public enum DecoratorType { {
public enum DecoratorType
{
None, None,
CurrentBranchHead, CurrentBranchHead,
LocalBranchHead, LocalBranchHead,
@ -9,12 +11,14 @@ namespace SourceGit.Models {
Tag, Tag,
} }
public class Decorator { public class Decorator
{
public DecoratorType Type { get; set; } = DecoratorType.None; public DecoratorType Type { get; set; } = DecoratorType.None;
public string Name { get; set; } = ""; public string Name { get; set; } = "";
} }
public static class DecoratorResources { public static class DecoratorResources
{
public static readonly IBrush[] Backgrounds = [ public static readonly IBrush[] Backgrounds = [
new SolidColorBrush(0xFF02C302), new SolidColorBrush(0xFF02C302),
new SolidColorBrush(0xFFFFB835), new SolidColorBrush(0xFFFFB835),

View file

@ -1,8 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace SourceGit.Models { namespace SourceGit.Models
public class DiffOption { {
public class DiffOption
{
public Change WorkingCopyChange => _workingCopyChange; public Change WorkingCopyChange => _workingCopyChange;
public bool IsUnstaged => _isUnstaged; public bool IsUnstaged => _isUnstaged;
public List<string> Revisions => _revisions; public List<string> Revisions => _revisions;
@ -14,12 +16,15 @@ namespace SourceGit.Models {
/// </summary> /// </summary>
/// <param name="change"></param> /// <param name="change"></param>
/// <param name="isUnstaged"></param> /// <param name="isUnstaged"></param>
public DiffOption(Change change, bool isUnstaged) { public DiffOption(Change change, bool isUnstaged)
{
_workingCopyChange = change; _workingCopyChange = change;
_isUnstaged = isUnstaged; _isUnstaged = isUnstaged;
if (isUnstaged) { if (isUnstaged)
switch (change.WorkTree) { {
switch (change.WorkTree)
{
case ChangeState.Added: case ChangeState.Added:
case ChangeState.Untracked: case ChangeState.Untracked:
_extra = "--no-index"; _extra = "--no-index";
@ -31,7 +36,9 @@ namespace SourceGit.Models {
_orgPath = change.OriginalPath; _orgPath = change.OriginalPath;
break; break;
} }
} else { }
else
{
_extra = "--cached"; _extra = "--cached";
_path = change.Path; _path = change.Path;
_orgPath = change.OriginalPath; _orgPath = change.OriginalPath;
@ -43,7 +50,8 @@ namespace SourceGit.Models {
/// </summary> /// </summary>
/// <param name="commit"></param> /// <param name="commit"></param>
/// <param name="change"></param> /// <param name="change"></param>
public DiffOption(Commit commit, Change change) { public DiffOption(Commit commit, Change change)
{
var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^"; var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
_revisions.Add(baseRevision); _revisions.Add(baseRevision);
_revisions.Add(commit.SHA); _revisions.Add(commit.SHA);
@ -56,7 +64,8 @@ namespace SourceGit.Models {
/// </summary> /// </summary>
/// <param name="commit"></param> /// <param name="commit"></param>
/// <param name="file"></param> /// <param name="file"></param>
public DiffOption(Commit commit, string file) { public DiffOption(Commit commit, string file)
{
var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^"; var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
_revisions.Add(baseRevision); _revisions.Add(baseRevision);
_revisions.Add(commit.SHA); _revisions.Add(commit.SHA);
@ -69,7 +78,8 @@ namespace SourceGit.Models {
/// <param name="baseRevision"></param> /// <param name="baseRevision"></param>
/// <param name="targetRevision"></param> /// <param name="targetRevision"></param>
/// <param name="change"></param> /// <param name="change"></param>
public DiffOption(string baseRevision, string targetRevision, Change change) { public DiffOption(string baseRevision, string targetRevision, Change change)
{
_revisions.Add(baseRevision); _revisions.Add(baseRevision);
_revisions.Add(targetRevision); _revisions.Add(targetRevision);
_path = change.Path; _path = change.Path;
@ -80,7 +90,8 @@ namespace SourceGit.Models {
/// Converts to diff command arguments. /// Converts to diff command arguments.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public override string ToString() { public override string ToString()
{
var builder = new StringBuilder(); var builder = new StringBuilder();
if (!string.IsNullOrEmpty(_extra)) builder.Append($"{_extra} "); if (!string.IsNullOrEmpty(_extra)) builder.Append($"{_extra} ");
foreach (var r in _revisions) builder.Append($"{r} "); foreach (var r in _revisions) builder.Append($"{r} ");
@ -92,11 +103,11 @@ namespace SourceGit.Models {
return builder.ToString(); return builder.ToString();
} }
private Change _workingCopyChange = null; private readonly Change _workingCopyChange = null;
private bool _isUnstaged = false; private readonly bool _isUnstaged = false;
private string _orgPath = string.Empty; private readonly string _orgPath = string.Empty;
private string _path = string.Empty; private readonly string _path = string.Empty;
private string _extra = string.Empty; private readonly string _extra = string.Empty;
private List<string> _revisions = new List<string>(); private readonly List<string> _revisions = new List<string>();
} }
} }

View file

@ -2,8 +2,10 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Models { namespace SourceGit.Models
public enum TextDiffLineType { {
public enum TextDiffLineType
{
None, None,
Normal, Normal,
Indicator, Indicator,
@ -11,13 +13,15 @@ namespace SourceGit.Models {
Deleted, Deleted,
} }
public class TextInlineRange { public class TextInlineRange
{
public int Start { get; set; } public int Start { get; set; }
public int Count { get; set; } public int Count { get; set; }
public TextInlineRange(int p, int n) { Start = p; Count = n; } public TextInlineRange(int p, int n) { Start = p; Count = n; }
} }
public class TextDiffLine { public class TextDiffLine
{
public TextDiffLineType Type { get; set; } = TextDiffLineType.None; public TextDiffLineType Type { get; set; } = TextDiffLineType.None;
public string Content { get; set; } = ""; public string Content { get; set; } = "";
public int OldLineNumber { get; set; } = 0; public int OldLineNumber { get; set; } = 0;
@ -28,7 +32,8 @@ namespace SourceGit.Models {
public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString();
public TextDiffLine() { } public TextDiffLine() { }
public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine) { public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine)
{
Type = type; Type = type;
Content = content; Content = content;
OldLineNumber = oldLine; OldLineNumber = oldLine;
@ -36,7 +41,8 @@ namespace SourceGit.Models {
} }
} }
public class TextDiffSelection { public class TextDiffSelection
{
public int StartLine { get; set; } = 0; public int StartLine { get; set; } = 0;
public int EndLine { get; set; } = 0; public int EndLine { get; set; } = 0;
public bool HasChanges { get; set; } = false; public bool HasChanges { get; set; } = false;
@ -44,17 +50,20 @@ namespace SourceGit.Models {
public int IgnoredAdds { get; set; } = 0; public int IgnoredAdds { get; set; } = 0;
public int IgnoredDeletes { get; set; } = 0; public int IgnoredDeletes { get; set; } = 0;
public bool IsInRange(int idx) { public bool IsInRange(int idx)
{
return idx >= StartLine - 1 && idx < EndLine; return idx >= StartLine - 1 && idx < EndLine;
} }
} }
public class TextDiff { public partial class TextDiff
{
public string File { get; set; } = string.Empty; public string File { get; set; } = string.Empty;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>(); public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public int MaxLineNumber = 0; public int MaxLineNumber = 0;
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) { public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
{
var isTracked = !string.IsNullOrEmpty(fileBlobGuid); var isTracked = !string.IsNullOrEmpty(fileBlobGuid);
var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000"; var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000";
@ -68,17 +77,22 @@ namespace SourceGit.Models {
var additions = selection.EndLine - selection.StartLine; var additions = selection.EndLine - selection.StartLine;
if (selection.StartLine != 1) additions++; if (selection.StartLine != 1) additions++;
if (revert) { if (revert)
{
var totalLines = Lines.Count - 1; var totalLines = Lines.Count - 1;
builder.Append($"@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@"); builder.Append($"@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@");
for (int i = 1; i <= totalLines; i++) { for (int i = 1; i <= totalLines; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type != TextDiffLineType.Added) continue; if (line.Type != TextDiffLineType.Added) continue;
builder.Append(selection.IsInRange(i) ? "\n+" : "\n ").Append(line.Content); builder.Append(selection.IsInRange(i) ? "\n+" : "\n ").Append(line.Content);
} }
} else { }
else
{
builder.Append("@@ -0,0 +0,").Append(additions).Append(" @@"); builder.Append("@@ -0,0 +0,").Append(additions).Append(" @@");
for (int i = selection.StartLine - 1; i < selection.EndLine; i++) { for (int i = selection.StartLine - 1; i < selection.EndLine; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type != TextDiffLineType.Added) continue; if (line.Type != TextDiffLineType.Added) continue;
builder.Append("\n+").Append(line.Content); builder.Append("\n+").Append(line.Content);
@ -89,7 +103,8 @@ namespace SourceGit.Models {
System.IO.File.WriteAllText(output, builder.ToString()); System.IO.File.WriteAllText(output, builder.ToString());
} }
public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, string output) { public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, string output)
{
var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path; var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path;
var builder = new StringBuilder(); var builder = new StringBuilder();
@ -100,19 +115,27 @@ namespace SourceGit.Models {
// If last line of selection is a change. Find one more line. // If last line of selection is a change. Find one more line.
var tail = null as string; var tail = null as string;
if (selection.EndLine < Lines.Count) { if (selection.EndLine < Lines.Count)
{
var lastLine = Lines[selection.EndLine - 1]; var lastLine = Lines[selection.EndLine - 1];
if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) { if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted)
for (int i = selection.EndLine; i < Lines.Count; i++) { {
for (int i = selection.EndLine; i < Lines.Count; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) break; if (line.Type == TextDiffLineType.Indicator) break;
if (revert) { if (revert)
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) { {
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added)
{
tail = line.Content; tail = line.Content;
break; break;
} }
} else { }
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) { else
{
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted)
{
tail = line.Content; tail = line.Content;
break; break;
} }
@ -122,11 +145,14 @@ namespace SourceGit.Models {
} }
// If the first line is not indicator. // If the first line is not indicator.
if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator) { if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator)
{
var indicator = selection.StartLine - 1; var indicator = selection.StartLine - 1;
for (int i = selection.StartLine - 2; i >= 0; i--) { for (int i = selection.StartLine - 2; i >= 0; i--)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) { if (line.Type == TextDiffLineType.Indicator)
{
indicator = i; indicator = i;
break; break;
} }
@ -134,41 +160,62 @@ namespace SourceGit.Models {
var ignoreAdds = 0; var ignoreAdds = 0;
var ignoreRemoves = 0; var ignoreRemoves = 0;
for (int i = 0; i < indicator; i++) { for (int i = 0; i < indicator; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Added) { if (line.Type == TextDiffLineType.Added)
{
ignoreAdds++; ignoreAdds++;
} else if (line.Type == TextDiffLineType.Deleted) { }
else if (line.Type == TextDiffLineType.Deleted)
{
ignoreRemoves++; ignoreRemoves++;
} }
} }
for (int i = indicator; i < selection.StartLine - 1; i++) { for (int i = indicator; i < selection.StartLine - 1; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) { if (line.Type == TextDiffLineType.Indicator)
{
ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, tail != null); ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, tail != null);
} else if (line.Type == TextDiffLineType.Added) { }
else if (line.Type == TextDiffLineType.Added)
{
if (revert) builder.Append("\n ").Append(line.Content); if (revert) builder.Append("\n ").Append(line.Content);
} else if (line.Type == TextDiffLineType.Deleted) { }
else if (line.Type == TextDiffLineType.Deleted)
{
if (!revert) builder.Append("\n ").Append(line.Content); if (!revert) builder.Append("\n ").Append(line.Content);
} else if (line.Type == TextDiffLineType.Normal) { }
else if (line.Type == TextDiffLineType.Normal)
{
builder.Append("\n ").Append(line.Content); builder.Append("\n ").Append(line.Content);
} }
} }
} }
// Outputs the selected lines. // Outputs the selected lines.
for (int i = selection.StartLine - 1; i < selection.EndLine; i++) { for (int i = selection.StartLine - 1; i < selection.EndLine; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) { if (line.Type == TextDiffLineType.Indicator)
if (!ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, tail != null)) { {
if (!ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, tail != null))
{
break; break;
} }
} else if (line.Type == TextDiffLineType.Normal) { }
else if (line.Type == TextDiffLineType.Normal)
{
builder.Append("\n ").Append(line.Content); builder.Append("\n ").Append(line.Content);
} else if (line.Type == TextDiffLineType.Added) { }
else if (line.Type == TextDiffLineType.Added)
{
builder.Append("\n+").Append(line.Content); builder.Append("\n+").Append(line.Content);
} else if (line.Type == TextDiffLineType.Deleted) { }
else if (line.Type == TextDiffLineType.Deleted)
{
builder.Append("\n-").Append(line.Content); builder.Append("\n-").Append(line.Content);
} }
} }
@ -178,7 +225,8 @@ namespace SourceGit.Models {
System.IO.File.WriteAllText(output, builder.ToString()); System.IO.File.WriteAllText(output, builder.ToString());
} }
public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, bool isOldSide, string output) { public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, bool isOldSide, string output)
{
var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path; var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path;
var builder = new StringBuilder(); var builder = new StringBuilder();
@ -189,19 +237,27 @@ namespace SourceGit.Models {
// If last line of selection is a change. Find one more line. // If last line of selection is a change. Find one more line.
var tail = null as string; var tail = null as string;
if (selection.EndLine < Lines.Count) { if (selection.EndLine < Lines.Count)
{
var lastLine = Lines[selection.EndLine - 1]; var lastLine = Lines[selection.EndLine - 1];
if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) { if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted)
for (int i = selection.EndLine; i < Lines.Count; i++) { {
for (int i = selection.EndLine; i < Lines.Count; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) break; if (line.Type == TextDiffLineType.Indicator) break;
if (revert) { if (revert)
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) { {
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added)
{
tail = line.Content; tail = line.Content;
break; break;
} }
} else { }
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) { else
{
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted)
{
tail = line.Content; tail = line.Content;
break; break;
} }
@ -211,11 +267,14 @@ namespace SourceGit.Models {
} }
// If the first line is not indicator. // If the first line is not indicator.
if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator) { if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator)
{
var indicator = selection.StartLine - 1; var indicator = selection.StartLine - 1;
for (int i = selection.StartLine - 2; i >= 0; i--) { for (int i = selection.StartLine - 2; i >= 0; i--)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) { if (line.Type == TextDiffLineType.Indicator)
{
indicator = i; indicator = i;
break; break;
} }
@ -223,55 +282,88 @@ namespace SourceGit.Models {
var ignoreAdds = 0; var ignoreAdds = 0;
var ignoreRemoves = 0; var ignoreRemoves = 0;
for (int i = 0; i < indicator; i++) { for (int i = 0; i < indicator; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Added) { if (line.Type == TextDiffLineType.Added)
{
ignoreAdds++; ignoreAdds++;
} else if (line.Type == TextDiffLineType.Deleted) { }
else if (line.Type == TextDiffLineType.Deleted)
{
ignoreRemoves++; ignoreRemoves++;
} }
} }
for (int i = indicator; i < selection.StartLine - 1; i++) { for (int i = indicator; i < selection.StartLine - 1; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) { if (line.Type == TextDiffLineType.Indicator)
{
ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, isOldSide, tail != null); ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, isOldSide, tail != null);
} else if (line.Type == TextDiffLineType.Added) { }
else if (line.Type == TextDiffLineType.Added)
{
if (revert) builder.Append("\n ").Append(line.Content); if (revert) builder.Append("\n ").Append(line.Content);
} else if (line.Type == TextDiffLineType.Deleted) { }
else if (line.Type == TextDiffLineType.Deleted)
{
if (!revert) builder.Append("\n ").Append(line.Content); if (!revert) builder.Append("\n ").Append(line.Content);
} else if (line.Type == TextDiffLineType.Normal) { }
else if (line.Type == TextDiffLineType.Normal)
{
builder.Append("\n ").Append(line.Content); builder.Append("\n ").Append(line.Content);
} }
} }
} }
// Outputs the selected lines. // Outputs the selected lines.
for (int i = selection.StartLine - 1; i < selection.EndLine; i++) { for (int i = selection.StartLine - 1; i < selection.EndLine; i++)
{
var line = Lines[i]; var line = Lines[i];
if (line.Type == TextDiffLineType.Indicator) { if (line.Type == TextDiffLineType.Indicator)
if (!ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, isOldSide, tail != null)) { {
if (!ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, isOldSide, tail != null))
{
break; break;
} }
} else if (line.Type == TextDiffLineType.Normal) { }
else if (line.Type == TextDiffLineType.Normal)
{
builder.Append("\n ").Append(line.Content); builder.Append("\n ").Append(line.Content);
} else if (line.Type == TextDiffLineType.Added) { }
if (isOldSide) { else if (line.Type == TextDiffLineType.Added)
if (revert) { {
if (isOldSide)
{
if (revert)
{
builder.Append("\n ").Append(line.Content); builder.Append("\n ").Append(line.Content);
} else { }
else
{
selection.IgnoredAdds++; selection.IgnoredAdds++;
} }
} else { }
else
{
builder.Append("\n+").Append(line.Content); builder.Append("\n+").Append(line.Content);
} }
} else if (line.Type == TextDiffLineType.Deleted) { }
if (isOldSide) { else if (line.Type == TextDiffLineType.Deleted)
{
if (isOldSide)
{
builder.Append("\n-").Append(line.Content); builder.Append("\n-").Append(line.Content);
} else { }
if (!revert) { else
{
if (!revert)
{
builder.Append("\n ").Append(line.Content); builder.Append("\n ").Append(line.Content);
} else { }
else
{
selection.IgnoredDeletes++; selection.IgnoredDeletes++;
} }
} }
@ -283,46 +375,66 @@ namespace SourceGit.Models {
System.IO.File.WriteAllText(output, builder.ToString()); System.IO.File.WriteAllText(output, builder.ToString());
} }
private bool ProcessIndicatorForPatch(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed) { [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
var indicatorRegex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@"); private static partial Regex indicatorRegex();
var match = indicatorRegex.Match(indicator.Content); private bool ProcessIndicatorForPatch(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed)
{
var match = indicatorRegex().Match(indicator.Content);
var oldStart = int.Parse(match.Groups[1].Value); var oldStart = int.Parse(match.Groups[1].Value);
var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds;
var oldCount = 0; var oldCount = 0;
var newCount = 0; var newCount = 0;
for (int i = idx + 1; i < end; i++) { for (int i = idx + 1; i < end; i++)
{
var test = Lines[i]; var test = Lines[i];
if (test.Type == TextDiffLineType.Indicator) break; if (test.Type == TextDiffLineType.Indicator) break;
if (test.Type == TextDiffLineType.Normal) { if (test.Type == TextDiffLineType.Normal)
{
oldCount++; oldCount++;
newCount++; newCount++;
} else if (test.Type == TextDiffLineType.Added) { }
if (i < start - 1) { else if (test.Type == TextDiffLineType.Added)
if (revert) { {
if (i < start - 1)
{
if (revert)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else { }
else
{
newCount++; newCount++;
} }
if (i == end - 1 && tailed) { if (i == end - 1 && tailed)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else if (test.Type == TextDiffLineType.Deleted) { }
if (i < start - 1) { else if (test.Type == TextDiffLineType.Deleted)
if (!revert) { {
if (i < start - 1)
{
if (!revert)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else { }
else
{
oldCount++; oldCount++;
} }
if (i == end - 1 && tailed) { if (i == end - 1 && tailed)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
@ -335,60 +447,84 @@ namespace SourceGit.Models {
return true; return true;
} }
private bool ProcessIndicatorForPatchSingleSide(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool isOldSide, bool tailed) { private bool ProcessIndicatorForPatchSingleSide(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool isOldSide, bool tailed)
var indicatorRegex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@"); {
var match = indicatorRegex.Match(indicator.Content); var match = indicatorRegex().Match(indicator.Content);
var oldStart = int.Parse(match.Groups[1].Value); var oldStart = int.Parse(match.Groups[1].Value);
var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds;
var oldCount = 0; var oldCount = 0;
var newCount = 0; var newCount = 0;
for (int i = idx + 1; i < end; i++) { for (int i = idx + 1; i < end; i++)
{
var test = Lines[i]; var test = Lines[i];
if (test.Type == TextDiffLineType.Indicator) break; if (test.Type == TextDiffLineType.Indicator) break;
if (test.Type == TextDiffLineType.Normal) { if (test.Type == TextDiffLineType.Normal)
{
oldCount++; oldCount++;
newCount++; newCount++;
} else if (test.Type == TextDiffLineType.Added) { }
if (i < start - 1) { else if (test.Type == TextDiffLineType.Added)
if (revert) { {
if (i < start - 1)
{
if (revert)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else { }
if (isOldSide) { else
if (revert) { {
if (isOldSide)
{
if (revert)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else { }
else
{
newCount++; newCount++;
} }
} }
if (i == end - 1 && tailed) { if (i == end - 1 && tailed)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else if (test.Type == TextDiffLineType.Deleted) { }
if (i < start - 1) { else if (test.Type == TextDiffLineType.Deleted)
if (!revert) { {
if (i < start - 1)
{
if (!revert)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} else { }
if (isOldSide) { else
{
if (isOldSide)
{
oldCount++; oldCount++;
} else { }
if (!revert) { else
{
if (!revert)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
} }
} }
if (i == end - 1 && tailed) { if (i == end - 1 && tailed)
{
newCount++; newCount++;
oldCount++; oldCount++;
} }
@ -402,17 +538,20 @@ namespace SourceGit.Models {
} }
} }
public class LFSDiff { public class LFSDiff
{
public LFSObject Old { get; set; } = new LFSObject(); public LFSObject Old { get; set; } = new LFSObject();
public LFSObject New { get; set; } = new LFSObject(); public LFSObject New { get; set; } = new LFSObject();
} }
public class BinaryDiff { public class BinaryDiff
{
public long OldSize { get; set; } = 0; public long OldSize { get; set; } = 0;
public long NewSize { get; set; } = 0; public long NewSize { get; set; } = 0;
} }
public class DiffResult { public class DiffResult
{
public bool IsBinary { get; set; } = false; public bool IsBinary { get; set; } = false;
public bool IsLFS { get; set; } = false; public bool IsLFS { get; set; } = false;
public TextDiff TextDiff { get; set; } = null; public TextDiff TextDiff { get; set; } = null;

View file

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class ExternalMergeTools { {
public class ExternalMergeTools
{
public int Type { get; set; } public int Type { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Exec { get; set; } public string Exec { get; set; }
@ -11,8 +13,10 @@ namespace SourceGit.Models {
public static List<ExternalMergeTools> Supported; public static List<ExternalMergeTools> Supported;
static ExternalMergeTools() { static ExternalMergeTools()
if (OperatingSystem.IsWindows()) { {
if (OperatingSystem.IsWindows())
{
Supported = new List<ExternalMergeTools>() { Supported = new List<ExternalMergeTools>() {
new ExternalMergeTools(0, "Custom", "", "", ""), new ExternalMergeTools(0, "Custom", "", "", ""),
new ExternalMergeTools(1, "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(1, "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
@ -22,7 +26,9 @@ namespace SourceGit.Models {
new ExternalMergeTools(5, "Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(5, "Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMergeTools(6, "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(6, "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
}; };
} else if (OperatingSystem.IsMacOS()) { }
else if (OperatingSystem.IsMacOS())
{
Supported = new List<ExternalMergeTools>() { Supported = new List<ExternalMergeTools>() {
new ExternalMergeTools(0, "Custom", "", "", ""), new ExternalMergeTools(0, "Custom", "", "", ""),
new ExternalMergeTools(1, "FileMerge", "/usr/bin/opendiff", "\"$BASE\" \"$LOCAL\" \"$REMOTE\" -ancestor \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(1, "FileMerge", "/usr/bin/opendiff", "\"$BASE\" \"$LOCAL\" \"$REMOTE\" -ancestor \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
@ -30,21 +36,26 @@ namespace SourceGit.Models {
new ExternalMergeTools(3, "KDiff3", "/Applications/kdiff3.app/Contents/MacOS/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(3, "KDiff3", "/Applications/kdiff3.app/Contents/MacOS/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMergeTools(4, "Beyond Compare 4", "/Applications/Beyond Compare.app/Contents/MacOS/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(4, "Beyond Compare 4", "/Applications/Beyond Compare.app/Contents/MacOS/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
}; };
} else if (OperatingSystem.IsLinux()) { }
else if (OperatingSystem.IsLinux())
{
Supported = new List<ExternalMergeTools>() { Supported = new List<ExternalMergeTools>() {
new ExternalMergeTools(0, "Custom", "", "", ""), new ExternalMergeTools(0, "Custom", "", "", ""),
new ExternalMergeTools(1, "Visual Studio Code", "/usr/share/code/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(1, "Visual Studio Code", "/usr/share/code/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
new ExternalMergeTools(2, "KDiff3", "/usr/bin/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(2, "KDiff3", "/usr/bin/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMergeTools(3, "Beyond Compare 4", "/usr/bin/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMergeTools(3, "Beyond Compare 4", "/usr/bin/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
}; };
} else { }
else
{
Supported = new List<ExternalMergeTools>() { Supported = new List<ExternalMergeTools>() {
new ExternalMergeTools(0, "Custom", "", "", ""), new ExternalMergeTools(0, "Custom", "", "", ""),
}; };
} }
} }
public ExternalMergeTools(int type, string name, string exec, string cmd, string diffCmd) { public ExternalMergeTools(int type, string name, string exec, string cmd, string diffCmd)
{
Type = type; Type = type;
Name = name; Name = name;
Exec = exec; Exec = exec;

View file

@ -1,25 +1,31 @@
namespace SourceGit.Models { namespace SourceGit.Models
public enum GitFlowBranchType { {
public enum GitFlowBranchType
{
None, None,
Feature, Feature,
Release, Release,
Hotfix, Hotfix,
} }
public class GitFlow { public class GitFlow
{
public string Feature { get; set; } public string Feature { get; set; }
public string Release { get; set; } public string Release { get; set; }
public string Hotfix { get; set; } public string Hotfix { get; set; }
public bool IsEnabled { public bool IsEnabled
get { {
get
{
return !string.IsNullOrEmpty(Feature) return !string.IsNullOrEmpty(Feature)
&& !string.IsNullOrEmpty(Release) && !string.IsNullOrEmpty(Release)
&& !string.IsNullOrEmpty(Hotfix); && !string.IsNullOrEmpty(Hotfix);
} }
} }
public GitFlowBranchType GetBranchType(string name) { public GitFlowBranchType GetBranchType(string name)
{
if (!IsEnabled) return GitFlowBranchType.None; if (!IsEnabled) return GitFlowBranchType.None;
if (name.StartsWith(Feature)) return GitFlowBranchType.Feature; if (name.StartsWith(Feature)) return GitFlowBranchType.Feature;
if (name.StartsWith(Release)) return GitFlowBranchType.Release; if (name.StartsWith(Release)) return GitFlowBranchType.Release;

View file

@ -1,5 +1,7 @@
namespace SourceGit.Models { namespace SourceGit.Models
public class LFSObject { {
public class LFSObject
{
public string Oid { get; set; } = string.Empty; public string Oid { get; set; } = string.Empty;
public long Size { get; set; } = 0; public long Size { get; set; } = 0;
} }

View file

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class Locale { {
public class Locale
{
public string Name { get; set; } public string Name { get; set; }
public string Key { get; set; } public string Key { get; set; }
@ -10,7 +12,8 @@ namespace SourceGit.Models {
new Locale("简体中文", "zh_CN"), new Locale("简体中文", "zh_CN"),
}; };
public Locale(string name, string key) { public Locale(string name, string key)
{
Name = name; Name = name;
Key = key; Key = key;
} }

View file

@ -1,10 +1,13 @@
namespace SourceGit.Models { namespace SourceGit.Models
public class Notification { {
public class Notification
{
public bool IsError { get; set; } = false; public bool IsError { get; set; } = false;
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
} }
public interface INotificationReceiver { public interface INotificationReceiver
{
void OnReceiveNotification(string ctx, Notification notice); void OnReceiveNotification(string ctx, Notification notice);
} }
} }

View file

@ -1,5 +1,7 @@
namespace SourceGit.Models { namespace SourceGit.Models
public enum ObjectType { {
public enum ObjectType
{
None, None,
Blob, Blob,
Tree, Tree,
@ -7,7 +9,8 @@
Commit, Commit,
} }
public class Object { public class Object
{
public string SHA { get; set; } public string SHA { get; set; }
public ObjectType Type { get; set; } public ObjectType Type { get; set; }
public string Path { get; set; } public string Path { get; set; }

View file

@ -1,28 +1,43 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Models { namespace SourceGit.Models
public class Remote { {
public partial class Remote
{
[GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$")]
private static partial Regex regex1();
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-\.]+\.git$")]
private static partial Regex regex2();
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$")]
private static partial Regex regex3();
private static readonly Regex[] URL_FORMATS = [ private static readonly Regex[] URL_FORMATS = [
new Regex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$"), regex1(),
new Regex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-\.]+\.git$"), regex2(),
new Regex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$"), regex3(),
]; ];
public string Name { get; set; } public string Name { get; set; }
public string URL { get; set; } public string URL { get; set; }
public static bool IsSSH(string url) { public static bool IsSSH(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false; if (string.IsNullOrWhiteSpace(url)) return false;
for (int i = 1; i < URL_FORMATS.Length; i++) { for (int i = 1; i < URL_FORMATS.Length; i++)
{
if (URL_FORMATS[i].IsMatch(url)) return true; if (URL_FORMATS[i].IsMatch(url)) return true;
} }
return false; return false;
} }
public static bool IsValidURL(string url) { public static bool IsValidURL(string url)
foreach (var fmt in URL_FORMATS) { {
foreach (var fmt in URL_FORMATS)
{
if (fmt.IsMatch(url)) return true; if (fmt.IsMatch(url)) return true;
} }
return false; return false;

View file

@ -1,17 +1,23 @@
namespace SourceGit.Models { namespace SourceGit.Models
public class RevisionBinaryFile { {
public class RevisionBinaryFile
{
public long Size { get; set; } = 0;
} }
public class RevisionTextFile { public class RevisionTextFile
{
public string FileName { get; set; } public string FileName { get; set; }
public string Content { get; set; } public string Content { get; set; }
} }
public class RevisionLFSObject { public class RevisionLFSObject
{
public LFSObject Object { get; set; } public LFSObject Object { get; set; }
} }
public class RevisionSubmodule { public class RevisionSubmodule
{
public string SHA { get; set; } public string SHA { get; set; }
} }
} }

View file

@ -1,7 +1,9 @@
using System; using System;
namespace SourceGit.Models { namespace SourceGit.Models
public class Stash { {
public class Stash
{
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
public string Name { get; set; } = ""; public string Name { get; set; } = "";

View file

@ -1,44 +1,54 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class StatisticsSample { {
public class StatisticsSample
{
public string Name { get; set; } public string Name { get; set; }
public int Count { get; set; } public int Count { get; set; }
} }
public class StatisticsReport { public class StatisticsReport
{
public int Total { get; set; } = 0; public int Total { get; set; } = 0;
public List<StatisticsSample> Samples { get; set; } = new List<StatisticsSample>(); public List<StatisticsSample> Samples { get; set; } = new List<StatisticsSample>();
public List<StatisticsSample> ByCommitter { get; set; } = new List<StatisticsSample>(); public List<StatisticsSample> ByCommitter { get; set; } = new List<StatisticsSample>();
public void AddCommit(int index, string committer) { public void AddCommit(int index, string committer)
{
Total++; Total++;
Samples[index].Count++; Samples[index].Count++;
if (_mapByCommitter.ContainsKey(committer)) { if (_mapByCommitter.ContainsKey(committer))
{
_mapByCommitter[committer].Count++; _mapByCommitter[committer].Count++;
} else { }
else
{
var sample = new StatisticsSample() { Name = committer, Count = 1 }; var sample = new StatisticsSample() { Name = committer, Count = 1 };
_mapByCommitter.Add(committer, sample); _mapByCommitter.Add(committer, sample);
ByCommitter.Add(sample); ByCommitter.Add(sample);
} }
} }
public void Complete() { public void Complete()
{
ByCommitter.Sort((l, r) => r.Count - l.Count); ByCommitter.Sort((l, r) => r.Count - l.Count);
_mapByCommitter.Clear(); _mapByCommitter.Clear();
} }
private Dictionary<string, StatisticsSample> _mapByCommitter = new Dictionary<string, StatisticsSample>(); private readonly Dictionary<string, StatisticsSample> _mapByCommitter = new Dictionary<string, StatisticsSample>();
} }
public class Statistics { public class Statistics
{
public StatisticsReport Year { get; set; } = new StatisticsReport(); public StatisticsReport Year { get; set; } = new StatisticsReport();
public StatisticsReport Month { get; set; } = new StatisticsReport(); public StatisticsReport Month { get; set; } = new StatisticsReport();
public StatisticsReport Week { get; set; } = new StatisticsReport(); public StatisticsReport Week { get; set; } = new StatisticsReport();
public Statistics() { public Statistics()
{
_utcStart = DateTime.UnixEpoch; _utcStart = DateTime.UnixEpoch;
_today = DateTime.Today; _today = DateTime.Today;
_thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second); _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second);
@ -59,16 +69,20 @@ namespace SourceGit.Models {
"Dec", "Dec",
]; ];
for (int i = 0; i < monthNames.Length; i++) { for (int i = 0; i < monthNames.Length; i++)
Year.Samples.Add(new StatisticsSample { {
Year.Samples.Add(new StatisticsSample
{
Name = monthNames[i], Name = monthNames[i],
Count = 0, Count = 0,
}); });
} }
var monthDays = DateTime.DaysInMonth(_today.Year, _today.Month); var monthDays = DateTime.DaysInMonth(_today.Year, _today.Month);
for (int i = 0; i < monthDays; i++) { for (int i = 0; i < monthDays; i++)
Month.Samples.Add(new StatisticsSample { {
Month.Samples.Add(new StatisticsSample
{
Name = $"{i + 1}", Name = $"{i + 1}",
Count = 0, Count = 0,
}); });
@ -84,40 +98,47 @@ namespace SourceGit.Models {
"SAT", "SAT",
]; ];
for (int i = 0; i < weekDayNames.Length; i++) { for (int i = 0; i < weekDayNames.Length; i++)
Week.Samples.Add(new StatisticsSample { {
Week.Samples.Add(new StatisticsSample
{
Name = weekDayNames[i], Name = weekDayNames[i],
Count = 0, Count = 0,
}); });
} }
} }
public string Since() { public string Since()
{
return _today.ToString("yyyy-01-01 00:00:00"); return _today.ToString("yyyy-01-01 00:00:00");
} }
public void AddCommit(string committer, double timestamp) { public void AddCommit(string committer, double timestamp)
{
var time = _utcStart.AddSeconds(timestamp).ToLocalTime(); var time = _utcStart.AddSeconds(timestamp).ToLocalTime();
if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0) { if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0)
{
Week.AddCommit((int)time.DayOfWeek, committer); Week.AddCommit((int)time.DayOfWeek, committer);
} }
if (time.Month == _today.Month) { if (time.Month == _today.Month)
{
Month.AddCommit(time.Day - 1, committer); Month.AddCommit(time.Day - 1, committer);
} }
Year.AddCommit(time.Month - 1, committer); Year.AddCommit(time.Month - 1, committer);
} }
public void Complete() { public void Complete()
{
Year.Complete(); Year.Complete();
Month.Complete(); Month.Complete();
Week.Complete(); Week.Complete();
} }
private DateTime _utcStart; private readonly DateTime _utcStart;
private DateTime _today; private readonly DateTime _today;
private DateTime _thisWeekStart; private readonly DateTime _thisWeekStart;
private DateTime _thisWeekEnd; private readonly DateTime _thisWeekEnd;
} }
} }

View file

@ -1,5 +1,7 @@
namespace SourceGit.Models { namespace SourceGit.Models
public class Tag { {
public class Tag
{
public string Name { get; set; } public string Name { get; set; }
public string SHA { get; set; } public string SHA { get; set; }
public bool IsFiltered { get; set; } public bool IsFiltered { get; set; }

View file

@ -1,19 +1,23 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class TextInlineChange { {
public class TextInlineChange
{
public int DeletedStart { get; set; } public int DeletedStart { get; set; }
public int DeletedCount { get; set; } public int DeletedCount { get; set; }
public int AddedStart { get; set; } public int AddedStart { get; set; }
public int AddedCount { get; set; } public int AddedCount { get; set; }
class Chunk { class Chunk
{
public int Hash; public int Hash;
public bool Modified; public bool Modified;
public int Start; public int Start;
public int Size; public int Size;
public Chunk(int hash, int start, int size) { public Chunk(int hash, int start, int size)
{
Hash = hash; Hash = hash;
Modified = false; Modified = false;
Start = start; Start = start;
@ -21,7 +25,8 @@ namespace SourceGit.Models {
} }
} }
enum Edit { enum Edit
{
None, None,
DeletedRight, DeletedRight,
DeletedLeft, DeletedLeft,
@ -29,7 +34,8 @@ namespace SourceGit.Models {
AddedLeft, AddedLeft,
} }
class EditResult { class EditResult
{
public Edit State; public Edit State;
public int DeleteStart; public int DeleteStart;
public int DeleteEnd; public int DeleteEnd;
@ -37,14 +43,16 @@ namespace SourceGit.Models {
public int AddEnd; public int AddEnd;
} }
public TextInlineChange(int dp, int dc, int ap, int ac) { public TextInlineChange(int dp, int dc, int ap, int ac)
{
DeletedStart = dp; DeletedStart = dp;
DeletedCount = dc; DeletedCount = dc;
AddedStart = ap; AddedStart = ap;
AddedCount = ac; AddedCount = ac;
} }
public static List<TextInlineChange> Compare(string oldValue, string newValue) { public static List<TextInlineChange> Compare(string oldValue, string newValue)
{
var hashes = new Dictionary<string, int>(); var hashes = new Dictionary<string, int>();
var chunksOld = MakeChunks(hashes, oldValue); var chunksOld = MakeChunks(hashes, oldValue);
var chunksNew = MakeChunks(hashes, newValue); var chunksNew = MakeChunks(hashes, newValue);
@ -59,8 +67,10 @@ namespace SourceGit.Models {
var posOld = 0; var posOld = 0;
var posNew = 0; var posNew = 0;
var last = null as TextInlineChange; var last = null as TextInlineChange;
do { do
while (posOld < sizeOld && posNew < sizeNew && !chunksOld[posOld].Modified && !chunksNew[posNew].Modified) { {
while (posOld < sizeOld && posNew < sizeNew && !chunksOld[posOld].Modified && !chunksNew[posNew].Modified)
{
posOld++; posOld++;
posNew++; posNew++;
} }
@ -79,10 +89,12 @@ namespace SourceGit.Models {
countOld, countOld,
countNew > 0 ? chunksNew[beginNew].Start : 0, countNew > 0 ? chunksNew[beginNew].Start : 0,
countNew); countNew);
if (last != null) { if (last != null)
{
var midSizeOld = diff.DeletedStart - last.DeletedStart - last.DeletedCount; var midSizeOld = diff.DeletedStart - last.DeletedStart - last.DeletedCount;
var midSizeNew = diff.AddedStart - last.AddedStart - last.AddedCount; var midSizeNew = diff.AddedStart - last.AddedStart - last.AddedCount;
if (midSizeOld == 1 && midSizeNew == 1) { if (midSizeOld == 1 && midSizeNew == 1)
{
last.DeletedCount += (1 + countOld); last.DeletedCount += (1 + countOld);
last.AddedCount += (1 + countNew); last.AddedCount += (1 + countNew);
continue; continue;
@ -96,15 +108,18 @@ namespace SourceGit.Models {
return ret; return ret;
} }
private static List<Chunk> MakeChunks(Dictionary<string, int> hashes, string text) { private static List<Chunk> MakeChunks(Dictionary<string, int> hashes, string text)
{
var start = 0; var start = 0;
var size = text.Length; var size = text.Length;
var chunks = new List<Chunk>(); var chunks = new List<Chunk>();
var delims = new HashSet<char>(" \t+-*/=!,:;.'\"/?|&#@%`<>()[]{}\\".ToCharArray()); var delims = new HashSet<char>(" \t+-*/=!,:;.'\"/?|&#@%`<>()[]{}\\".ToCharArray());
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++)
{
var ch = text[i]; var ch = text[i];
if (delims.Contains(ch)) { if (delims.Contains(ch))
{
if (start != i) AddChunk(chunks, hashes, text.Substring(start, i - start), start); if (start != i) AddChunk(chunks, hashes, text.Substring(start, i - start), start);
AddChunk(chunks, hashes, text.Substring(i, 1), i); AddChunk(chunks, hashes, text.Substring(i, 1), i);
start = i + 1; start = i + 1;
@ -115,43 +130,59 @@ namespace SourceGit.Models {
return chunks; return chunks;
} }
private static void CheckModified(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse) { private static void CheckModified(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse)
while (startOld < endOld && startNew < endNew && chunksOld[startOld].Hash == chunksNew[startNew].Hash) { {
while (startOld < endOld && startNew < endNew && chunksOld[startOld].Hash == chunksNew[startNew].Hash)
{
startOld++; startOld++;
startNew++; startNew++;
} }
while (startOld < endOld && startNew < endNew && chunksOld[endOld - 1].Hash == chunksNew[endNew - 1].Hash) { while (startOld < endOld && startNew < endNew && chunksOld[endOld - 1].Hash == chunksNew[endNew - 1].Hash)
{
endOld--; endOld--;
endNew--; endNew--;
} }
var lenOld = endOld - startOld; var lenOld = endOld - startOld;
var lenNew = endNew - startNew; var lenNew = endNew - startNew;
if (lenOld > 0 && lenNew > 0) { if (lenOld > 0 && lenNew > 0)
{
var rs = CheckModifiedEdit(chunksOld, startOld, endOld, chunksNew, startNew, endNew, forward, reverse); var rs = CheckModifiedEdit(chunksOld, startOld, endOld, chunksNew, startNew, endNew, forward, reverse);
if (rs.State == Edit.None) return; if (rs.State == Edit.None) return;
if (rs.State == Edit.DeletedRight && rs.DeleteStart - 1 > startOld) { if (rs.State == Edit.DeletedRight && rs.DeleteStart - 1 > startOld)
{
chunksOld[--rs.DeleteStart].Modified = true; chunksOld[--rs.DeleteStart].Modified = true;
} else if (rs.State == Edit.DeletedLeft && rs.DeleteEnd < endOld) { }
else if (rs.State == Edit.DeletedLeft && rs.DeleteEnd < endOld)
{
chunksOld[rs.DeleteEnd++].Modified = true; chunksOld[rs.DeleteEnd++].Modified = true;
} else if (rs.State == Edit.AddedRight && rs.AddStart - 1 > startNew) { }
else if (rs.State == Edit.AddedRight && rs.AddStart - 1 > startNew)
{
chunksNew[--rs.AddStart].Modified = true; chunksNew[--rs.AddStart].Modified = true;
} else if (rs.State == Edit.AddedLeft && rs.AddEnd < endNew) { }
else if (rs.State == Edit.AddedLeft && rs.AddEnd < endNew)
{
chunksNew[rs.AddEnd++].Modified = true; chunksNew[rs.AddEnd++].Modified = true;
} }
CheckModified(chunksOld, startOld, rs.DeleteStart, chunksNew, startNew, rs.AddStart, forward, reverse); CheckModified(chunksOld, startOld, rs.DeleteStart, chunksNew, startNew, rs.AddStart, forward, reverse);
CheckModified(chunksOld, rs.DeleteEnd, endOld, chunksNew, rs.AddEnd, endNew, forward, reverse); CheckModified(chunksOld, rs.DeleteEnd, endOld, chunksNew, rs.AddEnd, endNew, forward, reverse);
} else if (lenOld > 0) { }
else if (lenOld > 0)
{
for (int i = startOld; i < endOld; i++) chunksOld[i].Modified = true; for (int i = startOld; i < endOld; i++) chunksOld[i].Modified = true;
} else if (lenNew > 0) { }
else if (lenNew > 0)
{
for (int i = startNew; i < endNew; i++) chunksNew[i].Modified = true; for (int i = startNew; i < endNew; i++) chunksNew[i].Modified = true;
} }
} }
private static EditResult CheckModifiedEdit(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse) { private static EditResult CheckModifiedEdit(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse)
{
var lenOld = endOld - startOld; var lenOld = endOld - startOld;
var lenNew = endNew - startNew; var lenNew = endNew - startNew;
var max = lenOld + lenNew + 1; var max = lenOld + lenNew + 1;
@ -163,15 +194,20 @@ namespace SourceGit.Models {
forward[1 + half] = 0; forward[1 + half] = 0;
reverse[1 + half] = lenOld + 1; reverse[1 + half] = lenOld + 1;
for (int i = 0; i <= half; i++) { for (int i = 0; i <= half; i++)
{
for (int j = -i; j <= i; j += 2) { for (int j = -i; j <= i; j += 2)
{
var idx = j + half; var idx = j + half;
int o, n; int o, n;
if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1])) { if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1]))
{
o = forward[idx + 1]; o = forward[idx + 1];
rs.State = Edit.AddedRight; rs.State = Edit.AddedRight;
} else { }
else
{
o = forward[idx - 1] + 1; o = forward[idx - 1] + 1;
rs.State = Edit.DeletedRight; rs.State = Edit.DeletedRight;
} }
@ -180,21 +216,27 @@ namespace SourceGit.Models {
var startX = o; var startX = o;
var startY = n; var startY = n;
while (o < lenOld && n < lenNew && chunksOld[o + startOld].Hash == chunksNew[n + startNew].Hash) { while (o < lenOld && n < lenNew && chunksOld[o + startOld].Hash == chunksNew[n + startNew].Hash)
{
o++; o++;
n++; n++;
} }
forward[idx] = o; forward[idx] = o;
if (!deltaEven && j - delta >= -i + 1 && j - delta <= i - 1) { if (!deltaEven && j - delta >= -i + 1 && j - delta <= i - 1)
{
var revIdx = (j - delta) + half; var revIdx = (j - delta) + half;
var revOld = reverse[revIdx]; var revOld = reverse[revIdx];
int revNew = revOld - j; int revNew = revOld - j;
if (revOld <= o && revNew <= n) { if (revOld <= o && revNew <= n)
if (i == 0) { {
if (i == 0)
{
rs.State = Edit.None; rs.State = Edit.None;
} else { }
else
{
rs.DeleteStart = startX + startOld; rs.DeleteStart = startX + startOld;
rs.DeleteEnd = o + startOld; rs.DeleteEnd = o + startOld;
rs.AddStart = startY + startNew; rs.AddStart = startY + startNew;
@ -205,13 +247,17 @@ namespace SourceGit.Models {
} }
} }
for (int j = -i; j <= i; j += 2) { for (int j = -i; j <= i; j += 2)
{
var idx = j + half; var idx = j + half;
int o, n; int o, n;
if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1])) { if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1]))
{
o = reverse[idx + 1] - 1; o = reverse[idx + 1] - 1;
rs.State = Edit.DeletedLeft; rs.State = Edit.DeletedLeft;
} else { }
else
{
o = reverse[idx - 1]; o = reverse[idx - 1];
rs.State = Edit.AddedLeft; rs.State = Edit.AddedLeft;
} }
@ -220,21 +266,27 @@ namespace SourceGit.Models {
var endX = o; var endX = o;
var endY = n; var endY = n;
while (o > 0 && n > 0 && chunksOld[startOld + o - 1].Hash == chunksNew[startNew + n - 1].Hash) { while (o > 0 && n > 0 && chunksOld[startOld + o - 1].Hash == chunksNew[startNew + n - 1].Hash)
{
o--; o--;
n--; n--;
} }
reverse[idx] = o; reverse[idx] = o;
if (deltaEven && j + delta >= -i && j + delta <= i) { if (deltaEven && j + delta >= -i && j + delta <= i)
{
var forIdx = (j + delta) + half; var forIdx = (j + delta) + half;
var forOld = forward[forIdx]; var forOld = forward[forIdx];
int forNew = forOld - (j + delta); int forNew = forOld - (j + delta);
if (forOld >= o && forNew >= n) { if (forOld >= o && forNew >= n)
if (i == 0) { {
if (i == 0)
{
rs.State = Edit.None; rs.State = Edit.None;
} else { }
else
{
rs.DeleteStart = o + startOld; rs.DeleteStart = o + startOld;
rs.DeleteEnd = endX + startOld; rs.DeleteEnd = endX + startOld;
rs.AddStart = n + startNew; rs.AddStart = n + startNew;
@ -250,11 +302,15 @@ namespace SourceGit.Models {
return rs; return rs;
} }
private static void AddChunk(List<Chunk> chunks, Dictionary<string, int> hashes, string data, int start) { private static void AddChunk(List<Chunk> chunks, Dictionary<string, int> hashes, string data, int start)
{
int hash; int hash;
if (hashes.TryGetValue(data, out hash)) { if (hashes.TryGetValue(data, out hash))
{
chunks.Add(new Chunk(hash, start, data.Length)); chunks.Add(new Chunk(hash, start, data.Length));
} else { }
else
{
hash = hashes.Count; hash = hashes.Count;
hashes.Add(data, hash); hashes.Add(data, hash);
chunks.Add(new Chunk(hash, start, data.Length)); chunks.Add(new Chunk(hash, start, data.Length));

View file

@ -1,29 +1,37 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Models { namespace SourceGit.Models
public class User { {
public class User
{
public static User Invalid = new User(); public static User Invalid = new User();
public static Dictionary<string, User> Caches = new Dictionary<string, User>(); public static Dictionary<string, User> Caches = new Dictionary<string, User>();
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;
public override bool Equals(object obj) { public override bool Equals(object obj)
{
if (obj == null || !(obj is User)) return false; if (obj == null || !(obj is User)) return false;
var other = obj as User; var other = obj as User;
return Name == other.Name && Email == other.Email; return Name == other.Name && Email == other.Email;
} }
public override int GetHashCode() { public override int GetHashCode()
{
return base.GetHashCode(); return base.GetHashCode();
} }
public static User FindOrAdd(string data) { public static User FindOrAdd(string data)
if (Caches.ContainsKey(data)) { {
if (Caches.ContainsKey(data))
{
return Caches[data]; return Caches[data];
} else { }
var nameEndIdx = data.IndexOf('<'); else
{
var nameEndIdx = data.IndexOf('<', System.StringComparison.Ordinal);
var name = nameEndIdx >= 2 ? data.Substring(0, nameEndIdx - 1) : string.Empty; var name = nameEndIdx >= 2 ? data.Substring(0, nameEndIdx - 1) : string.Empty;
var email = data.Substring(nameEndIdx + 1); var email = data.Substring(nameEndIdx + 1);

View file

@ -3,8 +3,10 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SourceGit.Models { namespace SourceGit.Models
public interface IRepository { {
public interface IRepository
{
string FullPath { get; set; } string FullPath { get; set; }
string GitDir { get; set; } string GitDir { get; set; }
@ -16,8 +18,10 @@ namespace SourceGit.Models {
void RefreshStashes(); void RefreshStashes();
} }
public class Watcher : IDisposable { public class Watcher : IDisposable
public Watcher(IRepository repo) { {
public Watcher(IRepository repo)
{
_repo = repo; _repo = repo;
_wcWatcher = new FileSystemWatcher(); _wcWatcher = new FileSystemWatcher();
@ -45,23 +49,30 @@ namespace SourceGit.Models {
_timer = new Timer(Tick, null, 100, 100); _timer = new Timer(Tick, null, 100, 100);
} }
public void SetEnabled(bool enabled) { public void SetEnabled(bool enabled)
if (enabled) { {
if (enabled)
{
if (_lockCount > 0) _lockCount--; if (_lockCount > 0) _lockCount--;
} else { }
else
{
_lockCount++; _lockCount++;
} }
} }
public void MarkBranchDirtyManually() { public void MarkBranchDirtyManually()
{
_updateBranch = DateTime.Now.ToFileTime() - 1; _updateBranch = DateTime.Now.ToFileTime() - 1;
} }
public void MarkWorkingCopyDirtyManually() { public void MarkWorkingCopyDirtyManually()
{
_updateWC = DateTime.Now.ToFileTime() - 1; _updateWC = DateTime.Now.ToFileTime() - 1;
} }
public void Dispose() { public void Dispose()
{
_repoWatcher.EnableRaisingEvents = false; _repoWatcher.EnableRaisingEvents = false;
_repoWatcher.Created -= OnRepositoryChanged; _repoWatcher.Created -= OnRepositoryChanged;
_repoWatcher.Renamed -= OnRepositoryChanged; _repoWatcher.Renamed -= OnRepositoryChanged;
@ -82,23 +93,30 @@ namespace SourceGit.Models {
_timer = null; _timer = null;
} }
private void Tick(object sender) { private void Tick(object sender)
{
if (_lockCount > 0) return; if (_lockCount > 0) return;
var now = DateTime.Now.ToFileTime(); var now = DateTime.Now.ToFileTime();
if (_updateBranch > 0 && now > _updateBranch) { if (_updateBranch > 0 && now > _updateBranch)
{
_updateBranch = 0; _updateBranch = 0;
_updateWC = 0; _updateWC = 0;
if (_updateTags > 0) { if (_updateTags > 0)
{
_updateTags = 0; _updateTags = 0;
Task.Run(() => { Task.Run(() =>
{
_repo.RefreshTags(); _repo.RefreshTags();
_repo.RefreshBranches(); _repo.RefreshBranches();
_repo.RefreshCommits(); _repo.RefreshCommits();
}); });
} else { }
Task.Run(() => { else
{
Task.Run(() =>
{
_repo.RefreshBranches(); _repo.RefreshBranches();
_repo.RefreshCommits(); _repo.RefreshCommits();
}); });
@ -107,49 +125,64 @@ namespace SourceGit.Models {
Task.Run(_repo.RefreshWorkingCopyChanges); Task.Run(_repo.RefreshWorkingCopyChanges);
} }
if (_updateWC > 0 && now > _updateWC) { if (_updateWC > 0 && now > _updateWC)
{
_updateWC = 0; _updateWC = 0;
Task.Run(_repo.RefreshWorkingCopyChanges); Task.Run(_repo.RefreshWorkingCopyChanges);
} }
if (_updateSubmodules > 0 && now > _updateSubmodules) { if (_updateSubmodules > 0 && now > _updateSubmodules)
{
_updateSubmodules = 0; _updateSubmodules = 0;
_repo.RefreshSubmodules(); _repo.RefreshSubmodules();
} }
if (_updateStashes > 0 && now > _updateStashes) { if (_updateStashes > 0 && now > _updateStashes)
{
_updateStashes = 0; _updateStashes = 0;
_repo.RefreshStashes(); _repo.RefreshStashes();
} }
if (_updateTags > 0 && now > _updateTags) { if (_updateTags > 0 && now > _updateTags)
{
_updateTags = 0; _updateTags = 0;
_repo.RefreshTags(); _repo.RefreshTags();
_repo.RefreshCommits(); _repo.RefreshCommits();
} }
} }
private void OnRepositoryChanged(object o, FileSystemEventArgs e) { private void OnRepositoryChanged(object o, FileSystemEventArgs e)
{
if (string.IsNullOrEmpty(e.Name)) return; if (string.IsNullOrEmpty(e.Name)) return;
var name = e.Name.Replace("\\", "/"); var name = e.Name.Replace("\\", "/");
if (name.StartsWith("modules", StringComparison.Ordinal)) { if (name.StartsWith("modules", StringComparison.Ordinal))
{
_updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
} else if (name.StartsWith("refs/tags", StringComparison.Ordinal)) { }
else if (name.StartsWith("refs/tags", StringComparison.Ordinal))
{
_updateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); _updateTags = DateTime.Now.AddSeconds(.5).ToFileTime();
} else if (name.StartsWith("refs/stash", StringComparison.Ordinal)) { }
else if (name.StartsWith("refs/stash", StringComparison.Ordinal))
{
_updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); _updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime();
} else if (name.Equals("HEAD", StringComparison.Ordinal) || }
else if (name.Equals("HEAD", StringComparison.Ordinal) ||
name.StartsWith("refs/heads/", StringComparison.Ordinal) || name.StartsWith("refs/heads/", StringComparison.Ordinal) ||
name.StartsWith("refs/remotes/", StringComparison.Ordinal) || name.StartsWith("refs/remotes/", StringComparison.Ordinal) ||
name.StartsWith("worktrees/")) { name.StartsWith("worktrees/", StringComparison.Ordinal))
{
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime(); _updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
} else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal)) { }
else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal))
{
_updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); _updateWC = DateTime.Now.AddSeconds(1).ToFileTime();
} }
} }
private void OnWorkingCopyChanged(object o, FileSystemEventArgs e) { private void OnWorkingCopyChanged(object o, FileSystemEventArgs e)
{
if (string.IsNullOrEmpty(e.Name)) return; if (string.IsNullOrEmpty(e.Name)) return;
var name = e.Name.Replace("\\", "/"); var name = e.Name.Replace("\\", "/");
@ -157,7 +190,7 @@ namespace SourceGit.Models {
_updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); _updateWC = DateTime.Now.AddSeconds(1).ToFileTime();
} }
private IRepository _repo = null; private readonly IRepository _repo = null;
private FileSystemWatcher _repoWatcher = null; private FileSystemWatcher _repoWatcher = null;
private FileSystemWatcher _wcWatcher = null; private FileSystemWatcher _wcWatcher = null;
private Timer _timer = null; private Timer _timer = null;

View file

@ -1,29 +1,37 @@
using Avalonia; using System.Diagnostics;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
namespace SourceGit.Native { using Avalonia;
namespace SourceGit.Native
{
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
internal class Linux : OS.IBackend { internal class Linux : OS.IBackend
public void SetupFonts(AppBuilder builder) { {
#if USE_FONT_INTER public void SetupApp(AppBuilder builder)
{
#if USE_FONT_INTER
builder.WithInterFont(); builder.WithInterFont();
#endif #endif
} }
public string FindGitExecutable() { public string FindGitExecutable()
{
if (File.Exists("/usr/bin/git")) return "/usr/bin/git"; if (File.Exists("/usr/bin/git")) return "/usr/bin/git";
return string.Empty; return string.Empty;
} }
public string FindVSCode() { public string FindVSCode()
{
if (File.Exists("/usr/share/code/code")) return "/usr/share/code/code"; if (File.Exists("/usr/share/code/code")) return "/usr/share/code/code";
return string.Empty; return string.Empty;
} }
public void OpenBrowser(string url) { public void OpenBrowser(string url)
if (!File.Exists("/usr/bin/xdg-open")) { {
if (!File.Exists("/usr/bin/xdg-open"))
{
App.RaiseException("", $"You should install xdg-open first!"); App.RaiseException("", $"You should install xdg-open first!");
return; return;
} }
@ -31,38 +39,54 @@ namespace SourceGit.Native {
Process.Start("xdg-open", $"\"{url}\""); Process.Start("xdg-open", $"\"{url}\"");
} }
public void OpenInFileManager(string path, bool select) { public void OpenInFileManager(string path, bool select)
if (!File.Exists("/usr/bin/xdg-open")) { {
if (!File.Exists("/usr/bin/xdg-open"))
{
App.RaiseException("", $"You should install xdg-open first!"); App.RaiseException("", $"You should install xdg-open first!");
return; return;
} }
if (Directory.Exists(path)) { if (Directory.Exists(path))
{
Process.Start("xdg-open", $"\"{path}\""); Process.Start("xdg-open", $"\"{path}\"");
} else { }
else
{
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path);
if (Directory.Exists(dir)) { if (Directory.Exists(dir))
{
Process.Start("xdg-open", $"\"{dir}\""); Process.Start("xdg-open", $"\"{dir}\"");
} }
} }
} }
public void OpenTerminal(string workdir) { public void OpenTerminal(string workdir)
{
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir; var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
if (File.Exists("/usr/bin/gnome-terminal")) { if (File.Exists("/usr/bin/gnome-terminal"))
{
Process.Start("/usr/bin/gnome-terminal", $"--working-directory=\"{dir}\""); Process.Start("/usr/bin/gnome-terminal", $"--working-directory=\"{dir}\"");
} else if (File.Exists("/usr/bin/konsole")) { }
else if (File.Exists("/usr/bin/konsole"))
{
Process.Start("/usr/bin/konsole", $"--workdir \"{dir}\""); Process.Start("/usr/bin/konsole", $"--workdir \"{dir}\"");
} else if (File.Exists("/usr/bin/xfce4-terminal")) { }
else if (File.Exists("/usr/bin/xfce4-terminal"))
{
Process.Start("/usr/bin/xfce4-terminal", $"--working-directory=\"{dir}\""); Process.Start("/usr/bin/xfce4-terminal", $"--working-directory=\"{dir}\"");
} else { }
else
{
App.RaiseException("", $"Only supports gnome-terminal/konsole/xfce4-terminal!"); App.RaiseException("", $"Only supports gnome-terminal/konsole/xfce4-terminal!");
return; return;
} }
} }
public void OpenWithDefaultEditor(string file) { public void OpenWithDefaultEditor(string file)
if (!File.Exists("/usr/bin/xdg-open")) { {
if (!File.Exists("/usr/bin/xdg-open"))
{
App.RaiseException("", $"You should install xdg-open first!"); App.RaiseException("", $"You should install xdg-open first!");
return; return;
} }
@ -70,7 +94,8 @@ namespace SourceGit.Native {
var proc = Process.Start("xdg-open", $"\"{file}\""); var proc = Process.Start("xdg-open", $"\"{file}\"");
proc.WaitForExit(); proc.WaitForExit();
if (proc.ExitCode != 0) { if (proc.ExitCode != 0)
{
App.RaiseException("", $"Failed to open \"{file}\""); App.RaiseException("", $"Failed to open \"{file}\"");
} }

View file

@ -1,15 +1,20 @@
using Avalonia; using System.Diagnostics;
using Avalonia.Media;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text; using System.Text;
namespace SourceGit.Native { using Avalonia;
using Avalonia.Media;
namespace SourceGit.Native
{
[SupportedOSPlatform("macOS")] [SupportedOSPlatform("macOS")]
internal class MacOS : OS.IBackend { internal class MacOS : OS.IBackend
public void SetupFonts(AppBuilder builder) { {
builder.With(new FontManagerOptions() { public void SetupApp(AppBuilder builder)
{
builder.With(new FontManagerOptions()
{
DefaultFamilyName = "PingFang SC", DefaultFamilyName = "PingFang SC",
FontFallbacks = [ FontFallbacks = [
new FontFallback { FontFamily = new FontFamily("PingFang SC") } new FontFallback { FontFamily = new FontFamily("PingFang SC") }
@ -17,32 +22,41 @@ namespace SourceGit.Native {
}); });
} }
public string FindGitExecutable() { public string FindGitExecutable()
{
if (File.Exists("/usr/bin/git")) return "/usr/bin/git"; if (File.Exists("/usr/bin/git")) return "/usr/bin/git";
return string.Empty; return string.Empty;
} }
public string FindVSCode() { public string FindVSCode()
if (File.Exists("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code")) { {
if (File.Exists("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"))
{
return "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"; return "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code";
} }
return string.Empty; return string.Empty;
} }
public void OpenBrowser(string url) { public void OpenBrowser(string url)
{
Process.Start("open", url); Process.Start("open", url);
} }
public void OpenInFileManager(string path, bool select) { public void OpenInFileManager(string path, bool select)
if (Directory.Exists(path)) { {
if (Directory.Exists(path))
{
Process.Start("open", path); Process.Start("open", path);
} else if (File.Exists(path)) { }
else if (File.Exists(path))
{
Process.Start("open", $"\"{path}\" -R"); Process.Start("open", $"\"{path}\" -R");
} }
} }
public void OpenTerminal(string workdir) { public void OpenTerminal(string workdir)
{
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir; var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("on run argv"); builder.AppendLine("on run argv");
@ -59,7 +73,8 @@ namespace SourceGit.Native {
proc.Exited += (o, e) => File.Delete(tmp); proc.Exited += (o, e) => File.Delete(tmp);
} }
public void OpenWithDefaultEditor(string file) { public void OpenWithDefaultEditor(string file)
{
Process.Start("open", file); Process.Start("open", file);
} }
} }

View file

@ -1,11 +1,15 @@
using Avalonia; using System;
using System;
using System.Diagnostics; using System.Diagnostics;
namespace SourceGit.Native { using Avalonia;
public static class OS {
public interface IBackend { namespace SourceGit.Native
void SetupFonts(AppBuilder builder); {
public static class OS
{
public interface IBackend
{
void SetupApp(AppBuilder builder);
string FindGitExecutable(); string FindGitExecutable();
string FindVSCode(); string FindVSCode();
@ -16,62 +20,81 @@ namespace SourceGit.Native {
void OpenWithDefaultEditor(string file); void OpenWithDefaultEditor(string file);
} }
public static string GitInstallPath { public static string GitInstallPath
{
get; get;
set; set;
} }
public static string VSCodeExecutableFile { public static string VSCodeExecutableFile
{
get; get;
set; set;
} }
static OS() { static OS()
if (OperatingSystem.IsMacOS()) { {
if (OperatingSystem.IsMacOS())
{
_backend = new MacOS(); _backend = new MacOS();
VSCodeExecutableFile = _backend.FindVSCode(); VSCodeExecutableFile = _backend.FindVSCode();
} else if (OperatingSystem.IsWindows()) { }
else if (OperatingSystem.IsWindows())
{
_backend = new Windows(); _backend = new Windows();
VSCodeExecutableFile = _backend.FindVSCode(); VSCodeExecutableFile = _backend.FindVSCode();
} else if (OperatingSystem.IsLinux()) { }
else if (OperatingSystem.IsLinux())
{
_backend = new Linux(); _backend = new Linux();
VSCodeExecutableFile = _backend.FindVSCode(); VSCodeExecutableFile = _backend.FindVSCode();
} else { }
else
{
throw new Exception("Platform unsupported!!!"); throw new Exception("Platform unsupported!!!");
} }
} }
public static void SetupFonts(AppBuilder builder) { public static void SetupApp(AppBuilder builder)
_backend?.SetupFonts(builder); {
_backend?.SetupApp(builder);
} }
public static string FindGitExecutable() { public static string FindGitExecutable()
{
return _backend?.FindGitExecutable(); return _backend?.FindGitExecutable();
} }
public static void OpenInFileManager(string path, bool select = false) { public static void OpenInFileManager(string path, bool select = false)
{
_backend?.OpenInFileManager(path, select); _backend?.OpenInFileManager(path, select);
} }
public static void OpenBrowser(string url) { public static void OpenBrowser(string url)
{
_backend?.OpenBrowser(url); _backend?.OpenBrowser(url);
} }
public static void OpenTerminal(string workdir) { public static void OpenTerminal(string workdir)
{
_backend?.OpenTerminal(workdir); _backend?.OpenTerminal(workdir);
} }
public static void OpenWithDefaultEditor(string file) { public static void OpenWithDefaultEditor(string file)
{
_backend?.OpenWithDefaultEditor(file); _backend?.OpenWithDefaultEditor(file);
} }
public static void OpenInVSCode(string repo) { public static void OpenInVSCode(string repo)
if (string.IsNullOrEmpty(VSCodeExecutableFile)) { {
if (string.IsNullOrEmpty(VSCodeExecutableFile))
{
App.RaiseException(repo, "Visual Studio Code can NOT be found in your system!!!"); App.RaiseException(repo, "Visual Studio Code can NOT be found in your system!!!");
return; return;
} }
Process.Start(new ProcessStartInfo() { Process.Start(new ProcessStartInfo()
{
WorkingDirectory = repo, WorkingDirectory = repo,
FileName = VSCodeExecutableFile, FileName = VSCodeExecutableFile,
Arguments = $"\"{repo}\"", Arguments = $"\"{repo}\"",
@ -79,6 +102,6 @@ namespace SourceGit.Native {
}); });
} }
private static IBackend _backend = null; private static readonly IBackend _backend = null;
} }
} }

View file

@ -1,20 +1,25 @@
using Avalonia; using System;
using Avalonia.Media;
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text; using System.Text;
namespace SourceGit.Native { using Avalonia;
using Avalonia.Media;
namespace SourceGit.Native
{
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
internal class Windows : OS.IBackend { internal class Windows : OS.IBackend
{
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)] [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs); private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
public void SetupFonts(AppBuilder builder) { public void SetupApp(AppBuilder builder)
builder.With(new FontManagerOptions() { {
builder.With(new FontManagerOptions()
{
DefaultFamilyName = "Microsoft YaHei UI", DefaultFamilyName = "Microsoft YaHei UI",
FontFallbacks = [ FontFallbacks = [
new FontFallback { FontFamily = new FontFamily("Microsoft YaHei UI") } new FontFallback { FontFamily = new FontFamily("Microsoft YaHei UI") }
@ -22,18 +27,21 @@ namespace SourceGit.Native {
}); });
} }
public string FindGitExecutable() { public string FindGitExecutable()
{
var reg = Microsoft.Win32.RegistryKey.OpenBaseKey( var reg = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryHive.LocalMachine,
Microsoft.Win32.RegistryView.Registry64); Microsoft.Win32.RegistryView.Registry64);
var git = reg.OpenSubKey("SOFTWARE\\GitForWindows"); var git = reg.OpenSubKey("SOFTWARE\\GitForWindows");
if (git != null) { if (git != null)
{
return Path.Combine(git.GetValue("InstallPath") as string, "bin", "git.exe"); return Path.Combine(git.GetValue("InstallPath") as string, "bin", "git.exe");
} }
var builder = new StringBuilder("git.exe", 259); var builder = new StringBuilder("git.exe", 259);
if (!PathFindOnPath(builder, null)) { if (!PathFindOnPath(builder, null))
{
return null; return null;
} }
@ -43,43 +51,51 @@ namespace SourceGit.Native {
return exePath; return exePath;
} }
public string FindVSCode() { public string FindVSCode()
{
var root = Microsoft.Win32.RegistryKey.OpenBaseKey( var root = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? Microsoft.Win32.RegistryView.Registry64 : Microsoft.Win32.RegistryView.Registry32); Environment.Is64BitOperatingSystem ? Microsoft.Win32.RegistryView.Registry64 : Microsoft.Win32.RegistryView.Registry32);
var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1"); var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1");
if (vscode != null) { if (vscode != null)
{
return vscode.GetValue("DisplayIcon") as string; return vscode.GetValue("DisplayIcon") as string;
} }
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"); vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
if (vscode != null) { if (vscode != null)
{
return vscode.GetValue("DisplayIcon") as string; return vscode.GetValue("DisplayIcon") as string;
} }
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1"); vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1");
if (vscode != null) { if (vscode != null)
{
return vscode.GetValue("DisplayIcon") as string; return vscode.GetValue("DisplayIcon") as string;
} }
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"); vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
if (vscode != null) { if (vscode != null)
{
return vscode.GetValue("DisplayIcon") as string; return vscode.GetValue("DisplayIcon") as string;
} }
return string.Empty; return string.Empty;
} }
public void OpenBrowser(string url) { public void OpenBrowser(string url)
{
var info = new ProcessStartInfo("cmd", $"/c start {url}"); var info = new ProcessStartInfo("cmd", $"/c start {url}");
info.CreateNoWindow = true; info.CreateNoWindow = true;
Process.Start(info); Process.Start(info);
} }
public void OpenTerminal(string workdir) { public void OpenTerminal(string workdir)
{
var bash = Path.Combine(Path.GetDirectoryName(OS.GitInstallPath), "bash.exe"); var bash = Path.Combine(Path.GetDirectoryName(OS.GitInstallPath), "bash.exe");
if (!File.Exists(bash)) { if (!File.Exists(bash))
{
App.RaiseException(string.IsNullOrEmpty(workdir) ? "" : workdir, $"Can NOT found bash.exe under '{Path.GetDirectoryName(OS.GitInstallPath)}'"); App.RaiseException(string.IsNullOrEmpty(workdir) ? "" : workdir, $"Can NOT found bash.exe under '{Path.GetDirectoryName(OS.GitInstallPath)}'");
return; return;
} }
@ -91,22 +107,30 @@ namespace SourceGit.Native {
Process.Start(startInfo); Process.Start(startInfo);
} }
public void OpenInFileManager(string path, bool select) { public void OpenInFileManager(string path, bool select)
{
var fullpath = string.Empty; var fullpath = string.Empty;
if (File.Exists(path)) { if (File.Exists(path))
{
fullpath = new FileInfo(path).FullName; fullpath = new FileInfo(path).FullName;
} else { }
else
{
fullpath = new DirectoryInfo(path).FullName; fullpath = new DirectoryInfo(path).FullName;
} }
if (select) { if (select)
{
Process.Start("explorer", $"/select,\"{fullpath}\""); Process.Start("explorer", $"/select,\"{fullpath}\"");
} else { }
else
{
Process.Start("explorer", fullpath); Process.Start("explorer", fullpath);
} }
} }
public void OpenWithDefaultEditor(string file) { public void OpenWithDefaultEditor(string file)
{
var info = new FileInfo(file); var info = new FileInfo(file);
var start = new ProcessStartInfo("cmd", $"/c start {info.FullName}"); var start = new ProcessStartInfo("cmd", $"/c start {info.FullName}");
start.CreateNoWindow = true; start.CreateNoWindow = true;

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