mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
refactor<*>: rewrite all with AvaloniaUI
This commit is contained in:
parent
0136904612
commit
2a62596999
521 changed files with 19780 additions and 23244 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,5 +3,4 @@
|
|||
.vscode
|
||||
bin
|
||||
obj
|
||||
publish
|
||||
*.user
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# SourceGit
|
||||
|
||||
Opensouce Git GUI client for Windows.
|
||||
Opensouce Git GUI client.
|
||||
|
||||
## High-lights
|
||||
|
||||
* Opensource/Free
|
||||
* Light-weight
|
||||
* Fast
|
||||
* English/简体中文
|
||||
* Build-in light/dark themes
|
||||
|
@ -29,8 +28,6 @@ Opensouce Git GUI client for Windows.
|
|||
|
||||
Pre-build Binaries:[Releases](https://github.com/sourcegit-scm/sourcegit/releases)
|
||||
|
||||
> NOTE: You need install Git first.
|
||||
|
||||
## Screen Shots
|
||||
|
||||
* Drak Theme
|
||||
|
|
25
src/App.axaml
Normal file
25
src/App.axaml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SourceGit.App"
|
||||
Name="SourceGit"
|
||||
RequestedThemeVariant="Dark">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="/Resources/Fonts.axaml"/>
|
||||
<ResourceInclude Source="/Resources/Icons.axaml"/>
|
||||
<ResourceInclude Source="/Resources/Themes.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceInclude x:Key="en_US" Source="/Resources/Locales/en_US.axaml"/>
|
||||
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||
<StyleInclude Source="/Resources/Styles.axaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
158
src/App.axaml.cs
Normal file
158
src/App.axaml.cs
Normal file
|
@ -0,0 +1,158 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit {
|
||||
public partial class App : Application {
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args) {
|
||||
try {
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
} catch (Exception ex) {
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("Crash: ");
|
||||
builder.Append(ex.Message);
|
||||
builder.Append("\n\n");
|
||||
builder.Append("----------------------------\n");
|
||||
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
|
||||
builder.Append($"OS: {Environment.OSVersion.ToString()}\n");
|
||||
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
|
||||
builder.Append($"Source: {ex.Source}\n");
|
||||
builder.Append($"---------------------------\n\n");
|
||||
builder.Append(ex.StackTrace);
|
||||
|
||||
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||
var file = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
|
||||
file = Path.Combine(file, $"crash_{time}.log");
|
||||
File.WriteAllText(file, builder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp() {
|
||||
var builder = AppBuilder.Configure<App>();
|
||||
builder.UsePlatformDetect();
|
||||
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
builder.With(new FontManagerOptions() {
|
||||
FontFallbacks = [
|
||||
new FontFallback { FontFamily = new FontFamily("Microsoft YaHei UI") }
|
||||
]
|
||||
});
|
||||
} else if (OperatingSystem.IsMacOS()) {
|
||||
builder.With(new FontManagerOptions() {
|
||||
FontFallbacks = [
|
||||
new FontFallback { FontFamily = new FontFamily("PingFang SC") }
|
||||
]
|
||||
});
|
||||
builder.With(new MacOSPlatformOptions() {
|
||||
DisableDefaultApplicationMenuItems = true,
|
||||
DisableNativeMenus = true,
|
||||
});
|
||||
}
|
||||
|
||||
builder.LogToTrace();
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static void RaiseException(string context, string message) {
|
||||
if (Current is App app && app._notificationReceiver != null) {
|
||||
var ctx = context.Replace('\\', '/');
|
||||
var notice = new Models.Notification() { IsError = true, Message = message };
|
||||
app._notificationReceiver.OnReceiveNotification(ctx, notice);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendNotification(string context, string message) {
|
||||
if (Current is App app && app._notificationReceiver != null) {
|
||||
var ctx = context.Replace('\\', '/');
|
||||
var notice = new Models.Notification() { IsError = false, Message = message };
|
||||
app._notificationReceiver.OnReceiveNotification(ctx, notice);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetLocale(string localeKey) {
|
||||
var app = Current as App;
|
||||
var targetLocale = app.Resources[localeKey] as ResourceDictionary;
|
||||
if (targetLocale == null || targetLocale == app._activeLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (app._activeLocale != null) {
|
||||
app.Resources.MergedDictionaries.Remove(app._activeLocale);
|
||||
}
|
||||
|
||||
app.Resources.MergedDictionaries.Add(targetLocale);
|
||||
app._activeLocale = targetLocale;
|
||||
}
|
||||
|
||||
public static void SetTheme(string theme) {
|
||||
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) {
|
||||
App.Current.RequestedThemeVariant = ThemeVariant.Light;
|
||||
} else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) {
|
||||
App.Current.RequestedThemeVariant = ThemeVariant.Dark;
|
||||
} else {
|
||||
App.Current.RequestedThemeVariant = ThemeVariant.Default;
|
||||
}
|
||||
}
|
||||
|
||||
public static async void CopyText(string data) {
|
||||
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||
if (desktop.MainWindow.Clipboard is { } clipbord) {
|
||||
await clipbord.SetTextAsync(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string Text(string key, params object[] args) {
|
||||
var fmt = Current.FindResource($"Text.{key}") as string;
|
||||
if (string.IsNullOrWhiteSpace(fmt)) return $"Text.{key}";
|
||||
return string.Format(fmt, args);
|
||||
}
|
||||
|
||||
public static TopLevel GetTopLevel() {
|
||||
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||
return desktop.MainWindow;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void Quit() {
|
||||
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||
desktop.MainWindow.Close();
|
||||
desktop.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
SetLocale(ViewModels.Preference.Instance.Locale);
|
||||
SetTheme(ViewModels.Preference.Instance.Theme);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted() {
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||
BindingPlugins.DataValidators.RemoveAt(0);
|
||||
|
||||
var launcher = new Views.Launcher();
|
||||
_notificationReceiver = launcher;
|
||||
desktop.MainWindow = launcher;
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private ResourceDictionary _activeLocale = null;
|
||||
private Models.INotificationReceiver _notificationReceiver = null;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
</windowsSettings>
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embeded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="SourceGit.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
|
15
src/App.xaml
15
src/App.xaml
|
@ -1,15 +0,0 @@
|
|||
<Application x:Class="SourceGit.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Icons.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Converters.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Controls.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Themes/Light.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Locales/en_US.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
|
@ -1,99 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit {
|
||||
|
||||
/// <summary>
|
||||
/// 程序入口.
|
||||
/// </summary>
|
||||
public partial class App : Application {
|
||||
public static event Action<string, string> ExceptionRaised;
|
||||
|
||||
/// <summary>
|
||||
/// 读取本地化字串
|
||||
/// </summary>
|
||||
/// <param name="key">本地化字串的Key</param>
|
||||
/// <param name="args">可选格式化参数</param>
|
||||
/// <returns>本地化字串</returns>
|
||||
public static string Text(string key, params object[] args) {
|
||||
var data = Current.FindResource($"Text.{key}") as string;
|
||||
if (string.IsNullOrEmpty(data)) return $"Text.{key}";
|
||||
return string.Format(data, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发错误
|
||||
/// </summary>
|
||||
/// <param name="ctx">错误上下文</param>
|
||||
/// <param name="detail">错误内容</param>
|
||||
public static void Exception(string ctx, string detail) {
|
||||
ExceptionRaised?.Invoke(ctx, detail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
base.OnStartup(e);
|
||||
|
||||
// 崩溃文件生成
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, ev) => Models.CrashInfo.Create(ev.ExceptionObject as Exception);
|
||||
|
||||
// 创建必要目录
|
||||
if (!Directory.Exists(Views.Controls.Avatar.CACHE_PATH)) {
|
||||
Directory.CreateDirectory(Views.Controls.Avatar.CACHE_PATH);
|
||||
}
|
||||
|
||||
Models.Theme.Change();
|
||||
Models.Locale.Change();
|
||||
|
||||
// 如果启动命令中指定了路径,打开指定目录的仓库
|
||||
var launcher = new Views.Launcher();
|
||||
if (Models.Preference.Instance.IsReady) {
|
||||
if (e.Args.Length > 0) {
|
||||
var repo = Models.Preference.Instance.FindRepository(e.Args[0]);
|
||||
if (repo == null) {
|
||||
var path = new Commands.GetRepositoryRootPath(e.Args[0]).Result();
|
||||
if (path != null) {
|
||||
var gitDir = new Commands.QueryGitDir(path).Result();
|
||||
repo = Models.Preference.Instance.AddRepository(path, gitDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (repo != null) Models.Watcher.Open(repo);
|
||||
} else if (Models.Preference.Instance.Restore.IsEnabled) {
|
||||
var restore = Models.Preference.Instance.Restore;
|
||||
var actived = null as Models.Repository;
|
||||
if (restore.Opened.Count > 0) {
|
||||
foreach (var path in restore.Opened) {
|
||||
if (!Directory.Exists(path)) continue;
|
||||
var repo = Models.Preference.Instance.FindRepository(path);
|
||||
if (repo != null) Models.Watcher.Open(repo);
|
||||
if (path == restore.Actived) actived = repo;
|
||||
}
|
||||
|
||||
if (actived != null) Models.Watcher.Open(actived);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主界面显示
|
||||
MainWindow = launcher;
|
||||
MainWindow.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台运行
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
protected override void OnDeactivated(EventArgs e) {
|
||||
base.OnDeactivated(e);
|
||||
GC.Collect();
|
||||
Models.Preference.Save();
|
||||
}
|
||||
}
|
||||
}
|
81
src/Avalonia.Controls.DataGrid.patch
Normal file
81
src/Avalonia.Controls.DataGrid.patch
Normal file
|
@ -0,0 +1,81 @@
|
|||
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
|
||||
index e4573c3759a8a1eeece45c8bacb4fa853201f8e7..aa29066173e03092b61477985aed73beb08ec8fc 100644
|
||||
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
|
||||
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
|
||||
@@ -716,6 +716,16 @@ public DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode
|
||||
set { SetValue(RowDetailsVisibilityModeProperty, value); }
|
||||
}
|
||||
|
||||
+ public static readonly RoutedEvent<RoutedEventArgs> DisplayRegionChangedEvent = RoutedEvent.Register<DataGrid, RoutedEventArgs>(
|
||||
+ nameof(DisplayRegionChanged),
|
||||
+ RoutingStrategies.Bubble);
|
||||
+
|
||||
+ public event EventHandler<RoutedEventArgs> DisplayRegionChanged
|
||||
+ {
|
||||
+ add => AddHandler(DisplayRegionChangedEvent, value);
|
||||
+ remove => RemoveHandler(DisplayRegionChangedEvent, value);
|
||||
+ }
|
||||
+
|
||||
static DataGrid()
|
||||
{
|
||||
AffectsMeasure<DataGrid>(
|
||||
@@ -2428,6 +2438,11 @@ protected virtual void OnUnloadingRow(DataGridRowEventArgs e)
|
||||
}
|
||||
}
|
||||
|
||||
+ protected virtual void OnDisplayRegionChanged()
|
||||
+ {
|
||||
+ RaiseEvent(new RoutedEventArgs(DisplayRegionChangedEvent));
|
||||
+ }
|
||||
+
|
||||
/// <summary>
|
||||
/// Comparator class so we can sort list by the display index
|
||||
/// </summary>
|
||||
@@ -3879,6 +3894,7 @@ private void InvalidateColumnHeadersMeasure()
|
||||
{
|
||||
EnsureColumnHeadersVisibility();
|
||||
_columnHeadersPresenter.InvalidateMeasure();
|
||||
+ OnDisplayRegionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3903,6 +3919,8 @@ private void InvalidateRowsMeasure(bool invalidateIndividualElements)
|
||||
element.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
+
|
||||
+ OnDisplayRegionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6211,5 +6229,30 @@ protected virtual void OnAutoGeneratingColumn(DataGridAutoGeneratingColumnEventA
|
||||
{
|
||||
AutoGeneratingColumn?.Invoke(this, e);
|
||||
}
|
||||
+
|
||||
+ public Vector GetDisplayOffset()
|
||||
+ {
|
||||
+ // Has bug when using arrow keys via keyboard.
|
||||
+ // return new Vector(_horizontalOffset, _verticalOffset);
|
||||
+
|
||||
+ double startX = 0;
|
||||
+ double startY = 0;
|
||||
+
|
||||
+ foreach (var child in _rowsPresenter.Children)
|
||||
+ {
|
||||
+ var row = child as DataGridRow;
|
||||
+ if (row.Slot >= 0 && row.Bounds.Top <= 0 && row.Bounds.Top > -RowHeight)
|
||||
+ {
|
||||
+ var testY = RowHeight * row.Index - row.Bounds.Top;
|
||||
+ if (startY < testY)
|
||||
+ {
|
||||
+ startY = testY;
|
||||
+ startX = row.Bounds.Left;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return new Vector(startX, startY);
|
||||
+ }
|
||||
}
|
||||
}
|
1
src/BuildWindows.bat
Normal file
1
src/BuildWindows.bat
Normal file
|
@ -0,0 +1 @@
|
|||
dotnet publish -c Release -r win-x64 -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained
|
|
@ -1,27 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// `git add`命令
|
||||
/// </summary>
|
||||
public class Add : Command {
|
||||
public Add(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "add .";
|
||||
}
|
||||
public Add(string repo, List<Models.Change> changes = null) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
public Add(string repo, List<string> paths) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (changes == null || changes.Count == 0) {
|
||||
Args = "add .";
|
||||
} else {
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("add --");
|
||||
foreach (var p in paths) {
|
||||
foreach (var c in changes) {
|
||||
builder.Append(" \"");
|
||||
builder.Append(p);
|
||||
builder.Append(c.Path);
|
||||
builder.Append("\"");
|
||||
}
|
||||
|
||||
Cwd = repo;
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 应用Patch
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class Apply : Command {
|
||||
|
||||
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "apply ";
|
||||
if (ignoreWhitespace) Args += "--ignore-whitespace ";
|
||||
else Args += $"--whitespace={whitespaceMode} ";
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 存档命令
|
||||
/// </summary>
|
||||
public class Archive : Command {
|
||||
private Action<string> handler;
|
||||
|
||||
public Archive(string repo, string revision, string to, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
Args = $"archive --format=zip --verbose --output=\"{to}\" {revision}";
|
||||
public Archive(string repo, string revision, string saveTo, Action<string> outputHandler) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}";
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
_outputHandler = outputHandler;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,59 +2,59 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查看、添加或移除忽略变更文件
|
||||
/// </summary>
|
||||
public class AssumeUnchanged {
|
||||
private string repo;
|
||||
|
||||
class ViewCommand : Command {
|
||||
private static readonly Regex REG = new Regex(@"^(\w)\s+(.+)$");
|
||||
private List<string> outs = new List<string>();
|
||||
|
||||
public ViewCommand(string repo) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Args = "ls-files -v";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public List<string> Result() {
|
||||
Exec();
|
||||
return outs;
|
||||
return _outs;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
if (match.Groups[1].Value == "h") {
|
||||
outs.Add(match.Groups[2].Value);
|
||||
_outs.Add(match.Groups[2].Value);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> _outs = new List<string>();
|
||||
}
|
||||
|
||||
class ModCommand : Command {
|
||||
public ModCommand(string repo, string file, bool bAdd) {
|
||||
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
|
||||
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"update-index {mode} -- \"{file}\"";
|
||||
}
|
||||
}
|
||||
|
||||
public AssumeUnchanged(string repo) {
|
||||
this.repo = repo;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public List<string> View() {
|
||||
return new ViewCommand(repo).Result();
|
||||
return new ViewCommand(_repo).Result();
|
||||
}
|
||||
|
||||
public void Add(string file) {
|
||||
new ModCommand(repo, file, true).Exec();
|
||||
new ModCommand(_repo, file, true).Exec();
|
||||
}
|
||||
|
||||
public void Remove(string file) {
|
||||
new ModCommand(repo, file, false).Exec();
|
||||
new ModCommand(_repo, file, false).Exec();
|
||||
}
|
||||
|
||||
private string _repo;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +1,90 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 逐行追溯
|
||||
/// </summary>
|
||||
public class Blame : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)");
|
||||
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
private Data data = new Data();
|
||||
private bool needUnifyCommitSHA = false;
|
||||
private int minSHALen = 0;
|
||||
|
||||
public class Data {
|
||||
public List<Models.BlameLine> Lines = new List<Models.BlameLine>();
|
||||
public bool IsBinary = false;
|
||||
}
|
||||
|
||||
public Blame(string repo, string file, string revision) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"blame -t {revision} -- \"{file}\"";
|
||||
RaiseError = false;
|
||||
|
||||
_result.File = file;
|
||||
}
|
||||
|
||||
public Data Result() {
|
||||
Exec();
|
||||
public Models.BlameData Result() {
|
||||
var succ = Exec();
|
||||
if (!succ) {
|
||||
return new Models.BlameData();
|
||||
}
|
||||
|
||||
if (needUnifyCommitSHA) {
|
||||
foreach (var line in data.Lines) {
|
||||
if (line.CommitSHA.Length > minSHALen) {
|
||||
line.CommitSHA = line.CommitSHA.Substring(0, minSHALen);
|
||||
if (_needUnifyCommitSHA) {
|
||||
foreach (var line in _result.LineInfos) {
|
||||
if (line.CommitSHA.Length > _minSHALen) {
|
||||
line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
_result.Content = _content.ToString();
|
||||
return _result;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (data.IsBinary) return;
|
||||
protected override void OnReadline(string line) {
|
||||
if (_result.IsBinary) return;
|
||||
if (string.IsNullOrEmpty(line)) return;
|
||||
|
||||
if (line.IndexOf('\0') >= 0) {
|
||||
data.IsBinary = true;
|
||||
data.Lines.Clear();
|
||||
_result.IsBinary = true;
|
||||
_result.LineInfos.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
_content.AppendLine(match.Groups[4].Value);
|
||||
|
||||
var commit = match.Groups[1].Value;
|
||||
if (commit == _lastSHA) {
|
||||
var info = new Models.BlameLineInfo() {
|
||||
CommitSHA = commit,
|
||||
Author = string.Empty,
|
||||
Time = string.Empty,
|
||||
};
|
||||
|
||||
_result.LineInfos.Add(info);
|
||||
} else {
|
||||
var author = match.Groups[2].Value;
|
||||
var timestamp = int.Parse(match.Groups[3].Value);
|
||||
var content = match.Groups[4].Value;
|
||||
var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd");
|
||||
|
||||
var blameLine = new Models.BlameLine() {
|
||||
LineNumber = $"{data.Lines.Count + 1}",
|
||||
var blameLine = new Models.BlameLineInfo() {
|
||||
IsFirstInGroup = true,
|
||||
CommitSHA = commit,
|
||||
Author = author,
|
||||
Time = when,
|
||||
Content = content,
|
||||
};
|
||||
|
||||
if (line[0] == '^') {
|
||||
needUnifyCommitSHA = true;
|
||||
if (minSHALen == 0) minSHALen = commit.Length;
|
||||
else if (commit.Length < minSHALen) minSHALen = commit.Length;
|
||||
_lastSHA = commit;
|
||||
_result.LineInfos.Add(blameLine);
|
||||
}
|
||||
|
||||
data.Lines.Add(blameLine);
|
||||
if (line[0] == '^') {
|
||||
_needUnifyCommitSHA = true;
|
||||
_minSHALen = Math.Min(_minSHALen, commit.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private Models.BlameData _result = new Models.BlameData();
|
||||
private StringBuilder _content = new StringBuilder();
|
||||
private string _lastSHA = string.Empty;
|
||||
private bool _needUnifyCommitSHA = false;
|
||||
private int _minSHALen = 64;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 分支相关操作
|
||||
/// </summary>
|
||||
class Branch : Command {
|
||||
private string target = null;
|
||||
|
||||
public Branch(string repo, string branch) {
|
||||
Cwd = repo;
|
||||
target = branch;
|
||||
namespace SourceGit.Commands {
|
||||
public static class Branch {
|
||||
public static bool Create(string repo, string name, string basedOn) {
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
cmd.Args = $"branch {name} {basedOn}";
|
||||
return cmd.Exec();
|
||||
}
|
||||
|
||||
public void Create(string basedOn) {
|
||||
Args = $"branch {target} {basedOn}";
|
||||
Exec();
|
||||
public static bool Rename(string repo, string name, string to) {
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
cmd.Args = $"branch -M {name} {to}";
|
||||
return cmd.Exec();
|
||||
}
|
||||
|
||||
public void Rename(string to) {
|
||||
Args = $"branch -M {target} {to}";
|
||||
Exec();
|
||||
}
|
||||
|
||||
public void SetUpstream(string upstream) {
|
||||
Args = $"branch {target} ";
|
||||
public static bool SetUpstream(string repo, string name, string upstream) {
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
if (string.IsNullOrEmpty(upstream)) {
|
||||
Args += "--unset-upstream";
|
||||
cmd.Args = $"branch {name} --unset-upstream";
|
||||
} else {
|
||||
Args += $"-u {upstream}";
|
||||
cmd.Args = $"branch {name} -u {upstream}";
|
||||
}
|
||||
Exec();
|
||||
return cmd.Exec();
|
||||
}
|
||||
|
||||
public void Delete() {
|
||||
Args = $"branch -D {target}";
|
||||
Exec();
|
||||
public static bool Delete(string repo, string name) {
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
cmd.Args = $"branch -D {name}";
|
||||
return cmd.Exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,25 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 检出
|
||||
/// </summary>
|
||||
public class Checkout : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Checkout(string repo) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Branch(string branch, Action<string> onProgress) {
|
||||
Args = $"checkout --progress {branch}";
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
_outputHandler = onProgress;
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Branch(string branch, string basedOn, Action<string> onProgress) {
|
||||
Args = $"checkout --progress -b {branch} {basedOn}";
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
_outputHandler = onProgress;
|
||||
return Exec();
|
||||
}
|
||||
|
||||
|
@ -54,8 +50,10 @@ namespace SourceGit.Commands {
|
|||
return Exec();
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 遴选命令
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class CherryPick : Command {
|
||||
|
||||
public CherryPick(string repo, string commit, bool noCommit) {
|
||||
var mode = noCommit ? "-n" : "--ff";
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"cherry-pick {mode} {commit}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 清理指令
|
||||
/// </summary>
|
||||
public class Clean : Command {
|
||||
|
||||
public Clean(string repo) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "clean -qfd";
|
||||
}
|
||||
|
||||
|
@ -21,7 +18,8 @@ namespace SourceGit.Commands {
|
|||
builder.Append("\"");
|
||||
}
|
||||
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
/// </summary>
|
||||
public class Clone : Command {
|
||||
private Action<string> handler = null;
|
||||
private Action<string> onError = null;
|
||||
private Action<string> _notifyProgress;
|
||||
|
||||
public Clone(string path, string url, string localName, string sshKey, string extraArgs, Action<string> outputHandler, Action<string> errHandler) {
|
||||
Cwd = path;
|
||||
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler) {
|
||||
Context = ctx;
|
||||
WorkingDirectory = path;
|
||||
TraitErrorAsOutput = true;
|
||||
handler = outputHandler;
|
||||
onError = errHandler;
|
||||
|
||||
if (string.IsNullOrEmpty(sshKey)) {
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
} else {
|
||||
Args = "-c credential.helper=manager ";
|
||||
} else {
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
|
||||
Args += "clone --progress --verbose --recurse-submodules ";
|
||||
|
@ -26,14 +20,12 @@ namespace SourceGit.Commands {
|
|||
if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} ";
|
||||
Args += $"{url} ";
|
||||
if (!string.IsNullOrEmpty(localName)) Args += localName;
|
||||
|
||||
_notifyProgress = ouputHandler;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
|
||||
public override void OnException(string message) {
|
||||
onError?.Invoke(message);
|
||||
protected override void OnReadline(string line) {
|
||||
_notifyProgress?.Invoke(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
@ -5,60 +6,27 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 用于取消命令执行的上下文对象
|
||||
/// </summary>
|
||||
public class Context {
|
||||
public bool IsCancelRequested { get; set; } = false;
|
||||
public class Command {
|
||||
public class CancelToken {
|
||||
public bool Requested { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命令接口
|
||||
/// </summary>
|
||||
public class Command {
|
||||
private static readonly Regex PROGRESS_REG = new Regex(@"\d+%");
|
||||
|
||||
/// <summary>
|
||||
/// 读取全部输出时的结果
|
||||
/// </summary>
|
||||
public class ReadToEndResult {
|
||||
public bool IsSuccess { get; set; }
|
||||
public string Output { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string StdOut { get; set; }
|
||||
public string StdErr { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上下文
|
||||
/// </summary>
|
||||
public Context Ctx { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 运行路径
|
||||
/// </summary>
|
||||
public string Cwd { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 参数
|
||||
/// </summary>
|
||||
public string Args { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 是否忽略错误
|
||||
/// </summary>
|
||||
public bool DontRaiseError { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 使用标准错误输出
|
||||
/// </summary>
|
||||
public string Context { get; set; } = string.Empty;
|
||||
public CancelToken Cancel { get; set; } = null;
|
||||
public string WorkingDirectory { get; set; } = null;
|
||||
public string Args { get; set; } = string.Empty;
|
||||
public bool RaiseError { get; set; } = true;
|
||||
public bool TraitErrorAsOutput { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 运行
|
||||
/// </summary>
|
||||
public bool Exec() {
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = Models.Preference.Instance.Git.Path;
|
||||
start.FileName = Native.OS.GitExecutableFile;
|
||||
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
|
@ -67,49 +35,53 @@ namespace SourceGit.Commands {
|
|||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
|
||||
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
|
||||
|
||||
var errs = new List<string>();
|
||||
var proc = new Process() { StartInfo = start };
|
||||
var isCancelled = false;
|
||||
|
||||
proc.OutputDataReceived += (o, e) => {
|
||||
if (Ctx != null && Ctx.IsCancelRequested) {
|
||||
proc.OutputDataReceived += (_, e) => {
|
||||
if (Cancel != null && Cancel.Requested) {
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
if (!proc.HasExited) proc.Kill();
|
||||
if (!proc.HasExited) proc.Kill(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Data == null) return;
|
||||
OnReadline(e.Data);
|
||||
if (e.Data != null) OnReadline(e.Data);
|
||||
};
|
||||
proc.ErrorDataReceived += (o, e) => {
|
||||
if (Ctx != null && Ctx.IsCancelRequested) {
|
||||
|
||||
proc.ErrorDataReceived += (_, e) => {
|
||||
if (Cancel != null && Cancel.Requested) {
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
if (!proc.HasExited) proc.Kill();
|
||||
if (!proc.HasExited) proc.Kill(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(e.Data)) return;
|
||||
if (TraitErrorAsOutput) OnReadline(e.Data);
|
||||
|
||||
// 错误信息中忽略进度相关的输出
|
||||
// Ignore progress messages
|
||||
if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal)) return;
|
||||
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return;
|
||||
if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) return;
|
||||
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) return;
|
||||
if (PROGRESS_REG.IsMatch(e.Data)) return;
|
||||
if (_progressRegex.IsMatch(e.Data)) return;
|
||||
errs.Add(e.Data);
|
||||
};
|
||||
|
||||
try {
|
||||
proc.Start();
|
||||
} catch (Exception e) {
|
||||
if (!DontRaiseError) OnException(e.Message);
|
||||
if (RaiseError) {
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
App.RaiseException(Context, e.Message);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -121,19 +93,20 @@ namespace SourceGit.Commands {
|
|||
proc.Close();
|
||||
|
||||
if (!isCancelled && exitCode != 0 && errs.Count > 0) {
|
||||
if (!DontRaiseError) OnException(string.Join("\n", errs));
|
||||
if (RaiseError) {
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
App.RaiseException(Context, string.Join("\n", errs));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接读取全部标准输出
|
||||
/// </summary>
|
||||
public ReadToEndResult ReadToEnd() {
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = Models.Preference.Instance.Git.Path;
|
||||
start.FileName = Native.OS.GitExecutableFile;
|
||||
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
|
@ -142,22 +115,23 @@ namespace SourceGit.Commands {
|
|||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
|
||||
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
|
||||
|
||||
var proc = new Process() { StartInfo = start };
|
||||
try {
|
||||
proc.Start();
|
||||
} catch (Exception e) {
|
||||
return new ReadToEndResult() {
|
||||
Output = string.Empty,
|
||||
Error = e.Message,
|
||||
IsSuccess = false,
|
||||
StdOut = string.Empty,
|
||||
StdErr = e.Message,
|
||||
};
|
||||
}
|
||||
|
||||
var rs = new ReadToEndResult();
|
||||
rs.Output = proc.StandardOutput.ReadToEnd();
|
||||
rs.Error = proc.StandardError.ReadToEnd();
|
||||
var rs = new ReadToEndResult() {
|
||||
StdOut = proc.StandardOutput.ReadToEnd(),
|
||||
StdErr = proc.StandardError.ReadToEnd(),
|
||||
};
|
||||
|
||||
proc.WaitForExit();
|
||||
rs.IsSuccess = proc.ExitCode == 0;
|
||||
|
@ -166,19 +140,8 @@ namespace SourceGit.Commands {
|
|||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用Exec时的读取函数
|
||||
/// </summary>
|
||||
/// <param name="line"></param>
|
||||
public virtual void OnReadline(string line) {
|
||||
}
|
||||
protected virtual void OnReadline(string line) { }
|
||||
|
||||
/// <summary>
|
||||
/// 默认异常处理函数
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public virtual void OnException(string message) {
|
||||
App.Exception(Cwd, message);
|
||||
}
|
||||
private static readonly Regex _progressRegex = new Regex(@"\d+%");
|
||||
}
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
using System.IO;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// `git commit`命令
|
||||
/// </summary>
|
||||
public class Commit : Command {
|
||||
public Commit(string repo, string message, bool amend) {
|
||||
public Commit(string repo, string message, bool amend, bool allowEmpty = false) {
|
||||
var file = Path.GetTempFileName();
|
||||
File.WriteAllText(file, message);
|
||||
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"commit --file=\"{file}\"";
|
||||
if (amend) Args += " --amend --no-edit";
|
||||
if (allowEmpty) Args += " --allow-empty";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取得一个提交的变更列表
|
||||
/// </summary>
|
||||
public class CommitChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public CommitChanges(string cwd, string commit) {
|
||||
Cwd = cwd;
|
||||
Args = $"show --name-status {commit}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
|
||||
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
|
||||
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
|
||||
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
|
||||
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 对比两个提交间的变更
|
||||
/// </summary>
|
||||
public class CommitRangeChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public CommitRangeChanges(string cwd, string start, string end) {
|
||||
Cwd = cwd;
|
||||
Args = $"diff --name-status {start} {end}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
|
||||
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
|
||||
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
|
||||
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
|
||||
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
src/Commands/CompareRevisions.cs
Normal file
38
src/Commands/CompareRevisions.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class CompareRevisions : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
|
||||
public CompareRevisions(string repo, string start, string end) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff --name-status {start} {end}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
_changes.Sort((l, r) => l.Path.CompareTo(r.Path));
|
||||
return _changes;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
|
||||
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
|
||||
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
|
||||
case 'R': change.Set(Models.ChangeState.Renamed); _changes.Add(change); break;
|
||||
case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
|
@ -1,32 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// config命令
|
||||
/// </summary>
|
||||
public class Config : Command {
|
||||
public Config(string repository) {
|
||||
WorkingDirectory = repository;
|
||||
Context = repository;
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public Config() { }
|
||||
public Dictionary<string, string> ListAll() {
|
||||
Args = "config -l";
|
||||
|
||||
public Config(string repo) {
|
||||
Cwd = repo;
|
||||
var output = ReadToEnd();
|
||||
var rs = new Dictionary<string, string>();
|
||||
if (output.IsSuccess) {
|
||||
var lines = output.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines) {
|
||||
var idx = line.IndexOf('=');
|
||||
if (idx != -1) {
|
||||
var key = line.Substring(0, idx).Trim();
|
||||
var val = line.Substring(idx+1).Trim();
|
||||
if (rs.ContainsKey(key)) {
|
||||
rs[key] = val;
|
||||
} else {
|
||||
rs.Add(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
public string Get(string key) {
|
||||
Args = $"config {key}";
|
||||
return ReadToEnd().Output.Trim();
|
||||
return ReadToEnd().StdOut.Trim();
|
||||
}
|
||||
|
||||
public bool Set(string key, string val, bool allowEmpty = false) {
|
||||
if (!allowEmpty && string.IsNullOrEmpty(val)) {
|
||||
if (string.IsNullOrEmpty(Cwd)) {
|
||||
public bool Set(string key, string value, bool allowEmpty = false) {
|
||||
if (!allowEmpty && string.IsNullOrWhiteSpace(value)) {
|
||||
if (string.IsNullOrEmpty(WorkingDirectory)) {
|
||||
Args = $"config --global --unset {key}";
|
||||
} else {
|
||||
Args = $"config --unset {key}";
|
||||
}
|
||||
} else {
|
||||
if (string.IsNullOrEmpty(Cwd)) {
|
||||
Args = $"config --global {key} \"{val}\"";
|
||||
if (string.IsNullOrWhiteSpace(WorkingDirectory)) {
|
||||
Args = $"config --global {key} \"{value}\"";
|
||||
} else {
|
||||
Args = $"config {key} \"{val}\"";
|
||||
Args = $"config {key} \"{value}\"";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,111 +1,147 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// Diff命令(用于文件文件比对)
|
||||
/// </summary>
|
||||
public class Diff : Command {
|
||||
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
|
||||
private Models.TextChanges changes = new Models.TextChanges();
|
||||
private List<Models.TextChanges.Line> deleted = new List<Models.TextChanges.Line>();
|
||||
private List<Models.TextChanges.Line> added = new List<Models.TextChanges.Line>();
|
||||
private int oldLine = 0;
|
||||
private int newLine = 0;
|
||||
private int lineIndex = 0;
|
||||
private static readonly string PREFIX_LFS = " version https://git-lfs.github.com/spec/";
|
||||
|
||||
public Diff(string repo, string args) {
|
||||
Cwd = repo;
|
||||
Args = $"diff --ignore-cr-at-eol --unified=4 {args}";
|
||||
public Diff(string repo, Models.DiffOption opt) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff --ignore-cr-at-eol --unified=4 {opt}";
|
||||
}
|
||||
|
||||
public Models.TextChanges Result() {
|
||||
public Models.DiffResult Result() {
|
||||
Exec();
|
||||
ProcessChanges();
|
||||
if (changes.IsBinary) changes.Lines.Clear();
|
||||
lineIndex = 0;
|
||||
return changes;
|
||||
|
||||
if (_result.IsBinary || _result.IsLFS) {
|
||||
_result.TextDiff = null;
|
||||
} else {
|
||||
ProcessInlineHighlights();
|
||||
|
||||
if (_result.TextDiff.Lines.Count == 0) {
|
||||
_result.TextDiff = null;
|
||||
} else {
|
||||
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (changes.IsBinary) return;
|
||||
return _result;
|
||||
}
|
||||
|
||||
if (changes.Lines.Count == 0) {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (!match.Success) {
|
||||
if (line.StartsWith("Binary", StringComparison.Ordinal)) changes.IsBinary = true;
|
||||
protected override void OnReadline(string line) {
|
||||
if (_result.IsBinary) return;
|
||||
|
||||
if (_result.IsLFS) {
|
||||
var ch = line[0];
|
||||
if (ch == '-') {
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
_result.LFSDiff.Old.Oid = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
_result.LFSDiff.Old.Size = long.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (ch == '+') {
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
_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 ")) {
|
||||
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
oldLine = int.Parse(match.Groups[1].Value);
|
||||
newLine = int.Parse(match.Groups[2].Value);
|
||||
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Indicator, line, "", ""));
|
||||
if (_result.TextDiff.Lines.Count == 0) {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (!match.Success) {
|
||||
if (line.StartsWith("Binary", StringComparison.Ordinal)) _result.IsBinary = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_oldLine = int.Parse(match.Groups[1].Value);
|
||||
_newLine = int.Parse(match.Groups[2].Value);
|
||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, "", ""));
|
||||
} else {
|
||||
if (line.Length == 0) {
|
||||
ProcessChanges();
|
||||
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Normal, "", $"{oldLine}", $"{newLine}"));
|
||||
oldLine++;
|
||||
newLine++;
|
||||
ProcessInlineHighlights();
|
||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", $"{_oldLine}", $"{_newLine}"));
|
||||
_oldLine++;
|
||||
_newLine++;
|
||||
return;
|
||||
}
|
||||
|
||||
var ch = line[0];
|
||||
if (ch == '-') {
|
||||
deleted.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", ""));
|
||||
oldLine++;
|
||||
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), $"{_oldLine}", ""));
|
||||
_oldLine++;
|
||||
} else if (ch == '+') {
|
||||
added.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}"));
|
||||
newLine++;
|
||||
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), "", $"{_newLine}"));
|
||||
_newLine++;
|
||||
} else if (ch != '\\') {
|
||||
ProcessChanges();
|
||||
ProcessInlineHighlights();
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (match.Success) {
|
||||
oldLine = int.Parse(match.Groups[1].Value);
|
||||
newLine = int.Parse(match.Groups[2].Value);
|
||||
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Indicator, line, "", ""));
|
||||
_oldLine = int.Parse(match.Groups[1].Value);
|
||||
_newLine = int.Parse(match.Groups[2].Value);
|
||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, "", ""));
|
||||
} else {
|
||||
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}"));
|
||||
oldLine++;
|
||||
newLine++;
|
||||
if (line.StartsWith(PREFIX_LFS)) {
|
||||
_result.IsLFS = true;
|
||||
_result.LFSDiff = new Models.LFSDiff();
|
||||
return;
|
||||
}
|
||||
|
||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), $"{_oldLine}", $"{_newLine}"));
|
||||
_oldLine++;
|
||||
_newLine++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessChanges() {
|
||||
if (deleted.Any()) {
|
||||
if (added.Count == deleted.Count) {
|
||||
for (int i = added.Count - 1; i >= 0; i--) {
|
||||
var left = deleted[i];
|
||||
var right = added[i];
|
||||
private void ProcessInlineHighlights() {
|
||||
if (_deleted.Count > 0) {
|
||||
if (_added.Count == _deleted.Count) {
|
||||
for (int i = _added.Count - 1; i >= 0; i--) {
|
||||
var left = _deleted[i];
|
||||
var right = _added[i];
|
||||
|
||||
if (left.Content.Length > 1024 || right.Content.Length > 1024) continue;
|
||||
|
||||
var chunks = Models.TextCompare.Process(left.Content, right.Content);
|
||||
var chunks = Models.TextInlineChange.Compare(left.Content, right.Content);
|
||||
if (chunks.Count > 4) continue;
|
||||
|
||||
foreach (var chunk in chunks) {
|
||||
if (chunk.DeletedCount > 0) {
|
||||
left.Highlights.Add(new Models.TextChanges.HighlightRange(chunk.DeletedStart, chunk.DeletedCount));
|
||||
left.Highlights.Add(new Models.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount));
|
||||
}
|
||||
|
||||
if (chunk.AddedCount > 0) {
|
||||
right.Highlights.Add(new Models.TextChanges.HighlightRange(chunk.AddedStart, chunk.AddedCount));
|
||||
right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes.Lines.AddRange(deleted);
|
||||
deleted.Clear();
|
||||
_result.TextDiff.Lines.AddRange(_deleted);
|
||||
_deleted.Clear();
|
||||
}
|
||||
|
||||
if (added.Any()) {
|
||||
changes.Lines.AddRange(added);
|
||||
added.Clear();
|
||||
if (_added.Count > 0) {
|
||||
_result.TextDiff.Lines.AddRange(_added);
|
||||
_added.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() };
|
||||
private List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
|
||||
private List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
|
||||
private int _oldLine = 0;
|
||||
private int _newLine = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,19 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 忽略变更
|
||||
/// </summary>
|
||||
public class Discard {
|
||||
private string repo = null;
|
||||
|
||||
public Discard(string repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
public void Whole() {
|
||||
public static class Discard {
|
||||
public static void All(string repo) {
|
||||
new Reset(repo, "HEAD", "--hard").Exec();
|
||||
new Clean(repo).Exec();
|
||||
}
|
||||
|
||||
public void Changes(List<Models.Change> changes) {
|
||||
public static void Changes(string repo, List<Models.Change> changes) {
|
||||
var needClean = new List<string>();
|
||||
var needCheckout = new List<string>();
|
||||
|
||||
foreach (var c in changes) {
|
||||
if (c.WorkTree == Models.Change.Status.Untracked || c.WorkTree == Models.Change.Status.Added) {
|
||||
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) {
|
||||
needClean.Add(c.Path);
|
||||
} else {
|
||||
needCheckout.Add(c.Path);
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 拉取
|
||||
/// </summary>
|
||||
public class Fetch : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler) {
|
||||
Cwd = repo;
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
|
@ -24,12 +18,12 @@ namespace SourceGit.Commands {
|
|||
Args += "fetch --progress --verbose ";
|
||||
if (prune) Args += "--prune ";
|
||||
Args += remote;
|
||||
handler = outputHandler;
|
||||
AutoFetch.MarkFetched(repo);
|
||||
}
|
||||
|
||||
public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action<string> outputHandler) {
|
||||
Cwd = repo;
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
|
@ -40,63 +34,12 @@ namespace SourceGit.Commands {
|
|||
}
|
||||
|
||||
Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}";
|
||||
handler = outputHandler;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动拉取(每隔10分钟)
|
||||
/// </summary>
|
||||
public class AutoFetch {
|
||||
private static Dictionary<string, AutoFetch> jobs = new Dictionary<string, AutoFetch>();
|
||||
|
||||
private Fetch cmd = null;
|
||||
private long nextFetchPoint = 0;
|
||||
private Timer timer = null;
|
||||
|
||||
public static void Start(string repo) {
|
||||
if (!Models.Preference.Instance.Git.AutoFetchRemotes) return;
|
||||
|
||||
// 只自动更新加入管理列表中的仓库(子模块等不自动更新)
|
||||
var exists = Models.Preference.Instance.FindRepository(repo);
|
||||
if (exists == null) return;
|
||||
|
||||
var job = new AutoFetch(repo);
|
||||
jobs.Add(repo, job);
|
||||
}
|
||||
|
||||
public static void MarkFetched(string repo) {
|
||||
if (!jobs.ContainsKey(repo)) return;
|
||||
jobs[repo].nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
|
||||
}
|
||||
|
||||
public static void Stop(string repo) {
|
||||
if (!jobs.ContainsKey(repo)) return;
|
||||
|
||||
jobs[repo].timer.Dispose();
|
||||
jobs.Remove(repo);
|
||||
}
|
||||
|
||||
public AutoFetch(string repo) {
|
||||
cmd = new Fetch(repo, "--all", true, null);
|
||||
cmd.DontRaiseError = true;
|
||||
|
||||
nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
|
||||
timer = new Timer(OnTick, null, 60000, 10000);
|
||||
}
|
||||
|
||||
private void OnTick(object o) {
|
||||
var now = DateTime.Now.ToFileTime();
|
||||
if (nextFetchPoint > now) return;
|
||||
|
||||
Models.Watcher.SetEnabled(cmd.Cwd, false);
|
||||
cmd.Exec();
|
||||
nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
|
||||
Models.Watcher.SetEnabled(cmd.Cwd, true);
|
||||
}
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 将Commit另存为Patch文件
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class FormatPatch : Command {
|
||||
|
||||
public FormatPatch(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"format-patch {commit} -1 -o \"{path}\"";
|
||||
public FormatPatch(string repo, string commit, string saveTo) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"format-patch {commit} -1 -o \"{saveTo}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// GC
|
||||
/// </summary>
|
||||
public class GC : Command {
|
||||
private Action<string> handler;
|
||||
|
||||
public GC(string repo, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
Args = "gc";
|
||||
public GC(string repo, Action<string> outputHandler) {
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
Args = "gc";
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得一个库的根路径
|
||||
/// </summary>
|
||||
public class GetRepositoryRootPath : Command {
|
||||
public GetRepositoryRootPath(string path) {
|
||||
Cwd = path;
|
||||
Args = "rev-parse --show-toplevel";
|
||||
}
|
||||
|
||||
public string Result() {
|
||||
var rs = ReadToEnd().Output;
|
||||
if (string.IsNullOrEmpty(rs)) return null;
|
||||
return rs.Trim();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,22 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// Git-Flow命令
|
||||
/// </summary>
|
||||
public class GitFlow : Command {
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class GitFlow : Command {
|
||||
public GitFlow(string repo) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Init(string master, string develop, string feature, string release, string hotfix, string version) {
|
||||
var branches = new Branches(Cwd).Result();
|
||||
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 masterBranch = branches.Find(x => x.Name == master);
|
||||
if (masterBranch == null && current != null) new Branch(Cwd, develop).Create(current.Head);
|
||||
if (masterBranch == null && current != null) Branch.Create(WorkingDirectory, master, current.Head);
|
||||
|
||||
var devBranch = branches.Find(x => x.Name == develop);
|
||||
if (devBranch == null && current != null) new Branch(Cwd, develop).Create(current.Head);
|
||||
if (devBranch == null && current != null) Branch.Create(WorkingDirectory, develop, current.Head);
|
||||
|
||||
var cmd = new Config(Cwd);
|
||||
var cmd = new Config(WorkingDirectory);
|
||||
cmd.Set("gitflow.branch.master", master);
|
||||
cmd.Set("gitflow.branch.develop", develop);
|
||||
cmd.Set("gitflow.prefix.feature", feature);
|
||||
|
@ -32,7 +30,7 @@ namespace SourceGit.Commands {
|
|||
return Exec();
|
||||
}
|
||||
|
||||
public void Start(Models.GitFlowBranchType type, string name) {
|
||||
public bool Start(Models.GitFlowBranchType type, string name) {
|
||||
switch (type) {
|
||||
case Models.GitFlowBranchType.Feature:
|
||||
Args = $"flow feature start {name}";
|
||||
|
@ -44,13 +42,14 @@ namespace SourceGit.Commands {
|
|||
Args = $"flow hotfix start {name}";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
App.RaiseException(Context, "Bad branch type!!!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Exec();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public void Finish(Models.GitFlowBranchType type, string name, bool keepBranch) {
|
||||
public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch) {
|
||||
var option = keepBranch ? "-k" : string.Empty;
|
||||
switch (type) {
|
||||
case Models.GitFlowBranchType.Feature:
|
||||
|
@ -63,10 +62,11 @@ namespace SourceGit.Commands {
|
|||
Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\"";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
App.RaiseException(Context, "Bad branch type!!!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Exec();
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 初始化Git仓库
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class Init : Command {
|
||||
|
||||
public Init(string workDir) {
|
||||
Cwd = workDir;
|
||||
public Init(string ctx, string dir) {
|
||||
Context = ctx;
|
||||
WorkingDirectory = dir;
|
||||
Args = "init -q";
|
||||
}
|
||||
}
|
||||
|
|
18
src/Commands/IsBinary.cs
Normal file
18
src/Commands/IsBinary.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class IsBinary : Command {
|
||||
private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$");
|
||||
|
||||
public IsBinary(string repo, string commit, string path) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public bool Result() {
|
||||
return REG_TEST.IsMatch(ReadToEnd().StdOut);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查询指定版本下的某文件是否是二进制文件
|
||||
/// </summary>
|
||||
public class IsBinaryFile : Command {
|
||||
private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$");
|
||||
public IsBinaryFile(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
|
||||
}
|
||||
|
||||
public bool Result() {
|
||||
return REG_TEST.IsMatch(ReadToEnd().Output);
|
||||
}
|
||||
}
|
||||
}
|
15
src/Commands/IsLFSFiltered.cs
Normal file
15
src/Commands/IsLFSFiltered.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace SourceGit.Commands {
|
||||
public class IsLFSFiltered : Command {
|
||||
public IsLFSFiltered(string repo, string path) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"check-attr -a -z \"{path}\"";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public bool Result() {
|
||||
var rs = ReadToEnd();
|
||||
return rs.IsSuccess && rs.StdOut.Contains("filter\0lfs");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,51 +1,40 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// LFS相关
|
||||
/// </summary>
|
||||
public class LFS {
|
||||
private string repo;
|
||||
|
||||
private class PruneCmd : Command {
|
||||
private Action<string> handler;
|
||||
|
||||
class PruneCmd : Command {
|
||||
public PruneCmd(string repo, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "lfs prune";
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
_outputHandler = onProgress;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
|
||||
public LFS(string repo) {
|
||||
this.repo = repo;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var content = File.ReadAllText(path);
|
||||
return content.Contains("git lfs pre-push");
|
||||
}
|
||||
|
||||
public bool IsFiltered(string path) {
|
||||
var cmd = new Command();
|
||||
cmd.Cwd = repo;
|
||||
cmd.Args = $"check-attr -a -z \"{path}\"";
|
||||
|
||||
var rs = cmd.ReadToEnd();
|
||||
return rs.Output.Contains("filter\0lfs");
|
||||
public void Prune(Action<string> outputHandler) {
|
||||
new PruneCmd(_repo, outputHandler).Exec();
|
||||
}
|
||||
|
||||
public void Prune(Action<string> onProgress) {
|
||||
new PruneCmd(repo, onProgress).Exec();
|
||||
}
|
||||
private string _repo;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得本地工作副本变更
|
||||
/// </summary>
|
||||
public class LocalChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private static readonly string[] UNTRACKED = new string[] { "no", "all" };
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public LocalChanges(string path, bool includeUntracked = true) {
|
||||
Cwd = path;
|
||||
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
if (line.EndsWith("/", StringComparison.Ordinal)) return; // Ignore changes with git-worktree
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status) {
|
||||
case " M": change.Set(Models.Change.Status.None, Models.Change.Status.Modified); break;
|
||||
case " A": change.Set(Models.Change.Status.None, Models.Change.Status.Added); break;
|
||||
case " D": change.Set(Models.Change.Status.None, Models.Change.Status.Deleted); break;
|
||||
case " R": change.Set(Models.Change.Status.None, Models.Change.Status.Renamed); break;
|
||||
case " C": change.Set(Models.Change.Status.None, Models.Change.Status.Copied); break;
|
||||
case "M": change.Set(Models.Change.Status.Modified, Models.Change.Status.None); break;
|
||||
case "MM": change.Set(Models.Change.Status.Modified, Models.Change.Status.Modified); break;
|
||||
case "MD": change.Set(Models.Change.Status.Modified, Models.Change.Status.Deleted); break;
|
||||
case "A": change.Set(Models.Change.Status.Added, Models.Change.Status.None); break;
|
||||
case "AM": change.Set(Models.Change.Status.Added, Models.Change.Status.Modified); break;
|
||||
case "AD": change.Set(Models.Change.Status.Added, Models.Change.Status.Deleted); break;
|
||||
case "D": change.Set(Models.Change.Status.Deleted, Models.Change.Status.None); break;
|
||||
case "R": change.Set(Models.Change.Status.Renamed, Models.Change.Status.None); break;
|
||||
case "RM": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Modified); break;
|
||||
case "RD": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Deleted); break;
|
||||
case "C": change.Set(Models.Change.Status.Copied, Models.Change.Status.None); break;
|
||||
case "CM": change.Set(Models.Change.Status.Copied, Models.Change.Status.Modified); break;
|
||||
case "CD": change.Set(Models.Change.Status.Copied, Models.Change.Status.Deleted); break;
|
||||
case "DR": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Renamed); break;
|
||||
case "DC": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Copied); break;
|
||||
case "DD": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Deleted); break;
|
||||
case "AU": change.Set(Models.Change.Status.Added, Models.Change.Status.Unmerged); break;
|
||||
case "UD": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Deleted); break;
|
||||
case "UA": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Added); break;
|
||||
case "DU": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Unmerged); break;
|
||||
case "AA": change.Set(Models.Change.Status.Added, Models.Change.Status.Added); break;
|
||||
case "UU": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Unmerged); break;
|
||||
case "??": change.Set(Models.Change.Status.Untracked, Models.Change.Status.Untracked); break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
changes.Add(change);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,19 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 合并分支
|
||||
/// </summary>
|
||||
public class Merge : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Merge(string repo, string source, string mode, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
Args = $"merge --progress {source} {mode}";
|
||||
public Merge(string repo, string source, string mode, Action<string> outputHandler) {
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
Args = $"merge --progress {source} {mode}";
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler = null;
|
||||
}
|
||||
}
|
||||
|
|
41
src/Commands/MergeTool.cs
Normal file
41
src/Commands/MergeTool.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public static class MergeTool {
|
||||
public static bool OpenForMerge(string repo, string tool, string mergeCmd, string file) {
|
||||
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(mergeCmd)) {
|
||||
App.RaiseException(repo, "Invalid external merge tool settings!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(tool)) {
|
||||
App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.RaiseError = false;
|
||||
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{tool}\\\" {mergeCmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\"";
|
||||
return cmd.Exec();
|
||||
}
|
||||
|
||||
public static bool OpenForDiff(string repo, string tool, string diffCmd, Models.DiffOption option) {
|
||||
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(diffCmd)) {
|
||||
App.RaiseException(repo, "Invalid external merge tool settings!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(tool)) {
|
||||
App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.RaiseError = false;
|
||||
cmd.Args = $"-c difftool.sourcegit.cmd=\"\\\"{tool}\\\" {diffCmd}\" difftool --tool=sourcegit --no-prompt {option}";
|
||||
return cmd.Exec();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,12 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 拉回
|
||||
/// </summary>
|
||||
public class Pull : Command {
|
||||
private Action<string> handler = null;
|
||||
private bool needStash = false;
|
||||
|
||||
public Pull(string repo, string remote, string branch, bool useRebase, bool autoStash, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler) {
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
needStash = autoStash;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey)) {
|
||||
|
@ -27,25 +20,10 @@ namespace SourceGit.Commands {
|
|||
Args += $"{remote} {branch}";
|
||||
}
|
||||
|
||||
public bool Run() {
|
||||
if (needStash) {
|
||||
var changes = new LocalChanges(Cwd).Result();
|
||||
if (changes.Count > 0) {
|
||||
if (!new Stash(Cwd).Push(changes, "PULL_AUTO_STASH", true)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
needStash = false;
|
||||
}
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
var succ = Exec();
|
||||
if (succ && needStash) new Stash(Cwd).Pop("stash@{0}");
|
||||
return succ;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 推送
|
||||
/// </summary>
|
||||
public class Push : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
_outputHandler = onProgress;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey)) {
|
||||
|
@ -28,8 +24,16 @@ namespace SourceGit.Commands {
|
|||
Args += $"{remote} {local}:{remoteBranch}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only used to delete a remote branch!!!!!!
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="remote"></param>
|
||||
/// <param name="branch"></param>
|
||||
public Push(string repo, string remote, string branch) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey)) {
|
||||
|
@ -42,7 +46,8 @@ namespace SourceGit.Commands {
|
|||
}
|
||||
|
||||
public Push(string repo, string remote, string tag, bool isDelete) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey)) {
|
||||
|
@ -56,8 +61,10 @@ namespace SourceGit.Commands {
|
|||
Args += $"{remote} refs/tags/{tag}";
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 解析所有的分支
|
||||
/// </summary>
|
||||
public class Branches : Command {
|
||||
public class QueryBranches : Command {
|
||||
private static readonly string PREFIX_LOCAL = "refs/heads/";
|
||||
private static readonly string PREFIX_REMOTE = "refs/remotes/";
|
||||
private static readonly string CMD = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)\"";
|
||||
private static readonly Regex REG_AHEAD = new Regex(@"ahead (\d+)");
|
||||
private static readonly Regex REG_BEHIND = new Regex(@"behind (\d+)");
|
||||
|
||||
private List<Models.Branch> loaded = new List<Models.Branch>();
|
||||
|
||||
public Branches(string path) {
|
||||
Cwd = path;
|
||||
Args = CMD;
|
||||
public QueryBranches(string repo) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)\"";
|
||||
}
|
||||
|
||||
public List<Models.Branch> Result() {
|
||||
Exec();
|
||||
return loaded;
|
||||
return _branches;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
protected override void OnReadline(string line) {
|
||||
var parts = line.Split('$');
|
||||
if (parts.Length != 5) return;
|
||||
|
||||
|
@ -55,7 +50,7 @@ namespace SourceGit.Commands {
|
|||
branch.Upstream = parts[3];
|
||||
branch.UpstreamTrackStatus = ParseTrackStatus(parts[4]);
|
||||
|
||||
loaded.Add(branch);
|
||||
_branches.Add(branch);
|
||||
}
|
||||
|
||||
private string ParseTrackStatus(string data) {
|
||||
|
@ -75,5 +70,7 @@ namespace SourceGit.Commands {
|
|||
|
||||
return track.Trim();
|
||||
}
|
||||
|
||||
private List<Models.Branch> _branches = new List<Models.Branch>();
|
||||
}
|
||||
}
|
38
src/Commands/QueryCommitChanges.cs
Normal file
38
src/Commands/QueryCommitChanges.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class QueryCommitChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
|
||||
public QueryCommitChanges(string repo, string commitSHA) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"show --name-status {commitSHA}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
_changes.Sort((l, r) => l.Path.CompareTo(r.Path));
|
||||
return _changes;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
|
||||
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
|
||||
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
|
||||
case 'R': change.Set(Models.ChangeState.Renamed); _changes.Add(change); break;
|
||||
case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
|
@ -1,13 +1,8 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取得提交列表
|
||||
/// </summary>
|
||||
public class Commits : Command {
|
||||
public class QueryCommits : Command {
|
||||
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
|
||||
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
|
||||
|
||||
|
@ -17,8 +12,8 @@ namespace SourceGit.Commands {
|
|||
private bool isHeadFounded = false;
|
||||
private bool findFirstMerged = true;
|
||||
|
||||
public Commits(string path, string limits, bool needFindHead = true) {
|
||||
Cwd = path;
|
||||
public QueryCommits(string repo, string limits, bool needFindHead = true) {
|
||||
WorkingDirectory = repo;
|
||||
Args = "log --date-order --decorate=full --pretty=raw " + limits;
|
||||
findFirstMerged = needFindHead;
|
||||
}
|
||||
|
@ -38,7 +33,7 @@ namespace SourceGit.Commands {
|
|||
return commits;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
protected override void OnReadline(string line) {
|
||||
if (isSkipingGpgsig) {
|
||||
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false;
|
||||
return;
|
||||
|
@ -137,10 +132,10 @@ namespace SourceGit.Commands {
|
|||
}
|
||||
|
||||
private void MarkFirstMerged() {
|
||||
Args = $"log --since=\"{commits.Last().CommitterTimeStr}\" --format=\"%H\"";
|
||||
Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
|
||||
|
||||
var rs = ReadToEnd();
|
||||
var shas = rs.Output.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (shas.Length == 0) return;
|
||||
|
||||
var set = new HashSet<string>();
|
|
@ -1,26 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得指定提交下的某文件内容
|
||||
/// </summary>
|
||||
public class QueryFileContent : Command {
|
||||
private List<Models.TextLine> lines = new List<Models.TextLine>();
|
||||
private int added = 0;
|
||||
|
||||
public QueryFileContent(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"show {commit}:\"{path}\"";
|
||||
public QueryFileContent(string repo, string revision, string file) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"show {revision}:\"{file}\"";
|
||||
}
|
||||
|
||||
public List<Models.TextLine> Result() {
|
||||
public string Result() {
|
||||
Exec();
|
||||
return lines;
|
||||
return _builder.ToString();
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
added++;
|
||||
lines.Add(new Models.TextLine() { Number = added, Data = line });
|
||||
protected override void OnReadline(string line) {
|
||||
_builder.Append(line);
|
||||
_builder.Append('\n');
|
||||
}
|
||||
|
||||
private StringBuilder _builder = new StringBuilder();
|
||||
}
|
||||
}
|
||||
|
|
29
src/Commands/QueryFileSize.cs
Normal file
29
src/Commands/QueryFileSize.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
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 QueryFileSize(string repo, string file, string revision) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"ls-tree {revision} -l -- {file}";
|
||||
}
|
||||
|
||||
public long Result() {
|
||||
if (_result != 0) return _result;
|
||||
|
||||
var rs = ReadToEnd();
|
||||
if (rs.IsSuccess) {
|
||||
var match = REG_FORMAT.Match(rs.StdOut);
|
||||
if (match.Success) {
|
||||
return long.Parse(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private long _result = 0;
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查询文件大小变化
|
||||
/// </summary>
|
||||
public class QueryFileSizeChange {
|
||||
|
||||
class QuerySizeCmd : Command {
|
||||
public QuerySizeCmd(string repo, string path, string revision) {
|
||||
Cwd = repo;
|
||||
Args = $"cat-file -s {revision}:\"{path}\"";
|
||||
}
|
||||
|
||||
public long Result() {
|
||||
string data = ReadToEnd().Output;
|
||||
long size;
|
||||
if (!long.TryParse(data, out size)) size = 0;
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
private Models.FileSizeChange change = new Models.FileSizeChange();
|
||||
|
||||
public QueryFileSizeChange(string repo, string[] revisions, string path, string orgPath) {
|
||||
if (revisions.Length == 0) {
|
||||
change.NewSize = new FileInfo(Path.Combine(repo, path)).Length;
|
||||
change.OldSize = new QuerySizeCmd(repo, path, "HEAD").Result();
|
||||
} else if (revisions.Length == 1) {
|
||||
change.NewSize = new QuerySizeCmd(repo, path, "HEAD").Result();
|
||||
if (string.IsNullOrEmpty(orgPath)) {
|
||||
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
|
||||
} else {
|
||||
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
|
||||
}
|
||||
} else {
|
||||
change.NewSize = new QuerySizeCmd(repo, path, revisions[1]).Result();
|
||||
if (string.IsNullOrEmpty(orgPath)) {
|
||||
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
|
||||
} else {
|
||||
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Models.FileSizeChange Result() {
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,20 @@
|
|||
using System.IO;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取得GitDir
|
||||
/// </summary>
|
||||
public class QueryGitDir : Command {
|
||||
public QueryGitDir(string workDir) {
|
||||
Cwd = workDir;
|
||||
WorkingDirectory = workDir;
|
||||
Args = "rev-parse --git-dir";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public string Result() {
|
||||
var rs = ReadToEnd().Output;
|
||||
var rs = ReadToEnd().StdOut;
|
||||
if (string.IsNullOrEmpty(rs)) return null;
|
||||
|
||||
rs = rs.Trim();
|
||||
if (Path.IsPathRooted(rs)) return rs;
|
||||
return Path.GetFullPath(Path.Combine(Cwd, rs));
|
||||
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得一个LFS对象的信息
|
||||
/// </summary>
|
||||
public class QueryLFSObject : Command {
|
||||
private Models.LFSObject obj = new Models.LFSObject();
|
||||
|
||||
public QueryLFSObject(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"show {commit}:\"{path}\"";
|
||||
}
|
||||
|
||||
public Models.LFSObject Result() {
|
||||
Exec();
|
||||
return obj;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) {
|
||||
obj.OID = line.Substring(11).Trim();
|
||||
} else if (line.StartsWith("size")) {
|
||||
obj.Size = int.Parse(line.Substring(4).Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查询LFS对象变更
|
||||
/// </summary>
|
||||
public class QueryLFSObjectChange : Command {
|
||||
private Models.LFSChange change = new Models.LFSChange();
|
||||
|
||||
public QueryLFSObjectChange(string repo, string args) {
|
||||
Cwd = repo;
|
||||
Args = $"diff --ignore-cr-at-eol {args}";
|
||||
}
|
||||
|
||||
public Models.LFSChange Result() {
|
||||
Exec();
|
||||
return change;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var ch = line[0];
|
||||
if (ch == '-') {
|
||||
if (change.Old == null) change.Old = new Models.LFSObject();
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
change.Old.OID = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
change.Old.Size = int.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (ch == '+') {
|
||||
if (change.New == null) change.New = new Models.LFSObject();
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
change.New.OID = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
change.New.Size = int.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (line.StartsWith(" size ")) {
|
||||
change.New.Size = change.Old.Size = int.Parse(line.Substring(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
src/Commands/QueryLocalChanges.cs
Normal file
66
src/Commands/QueryLocalChanges.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class QueryLocalChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private static readonly string[] UNTRACKED = [ "no", "all" ];
|
||||
|
||||
public QueryLocalChanges(string repo, bool includeUntracked = true) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return _changes;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
if (line.EndsWith("/", StringComparison.Ordinal)) return; // Ignore changes with git-worktree
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status) {
|
||||
case " M": change.Set(Models.ChangeState.None, Models.ChangeState.Modified); break;
|
||||
case " A": change.Set(Models.ChangeState.None, Models.ChangeState.Added); break;
|
||||
case " D": change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); break;
|
||||
case " R": change.Set(Models.ChangeState.None, Models.ChangeState.Renamed); break;
|
||||
case " C": change.Set(Models.ChangeState.None, Models.ChangeState.Copied); break;
|
||||
case "M": change.Set(Models.ChangeState.Modified, Models.ChangeState.None); break;
|
||||
case "MM": change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); break;
|
||||
case "MD": change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); break;
|
||||
case "A": change.Set(Models.ChangeState.Added, Models.ChangeState.None); break;
|
||||
case "AM": change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); break;
|
||||
case "AD": change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); break;
|
||||
case "D": change.Set(Models.ChangeState.Deleted, Models.ChangeState.None); break;
|
||||
case "R": change.Set(Models.ChangeState.Renamed, Models.ChangeState.None); break;
|
||||
case "RM": change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); break;
|
||||
case "RD": change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); break;
|
||||
case "C": change.Set(Models.ChangeState.Copied, Models.ChangeState.None); break;
|
||||
case "CM": change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); break;
|
||||
case "CD": change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); break;
|
||||
case "DR": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed); break;
|
||||
case "DC": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied); break;
|
||||
case "DD": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted); break;
|
||||
case "AU": change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged); break;
|
||||
case "UD": change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted); break;
|
||||
case "UA": change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added); break;
|
||||
case "DU": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged); break;
|
||||
case "AA": change.Set(Models.ChangeState.Added, Models.ChangeState.Added); break;
|
||||
case "UU": change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged); break;
|
||||
case "??": change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked); break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
_changes.Add(change);
|
||||
}
|
||||
|
||||
private List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
|
@ -1,25 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 获取远程列表
|
||||
/// </summary>
|
||||
public class Remotes : Command {
|
||||
public class QueryRemotes : Command {
|
||||
private static readonly Regex REG_REMOTE = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
|
||||
private List<Models.Remote> loaded = new List<Models.Remote>();
|
||||
|
||||
public Remotes(string repo) {
|
||||
Cwd = repo;
|
||||
public QueryRemotes(string repo) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "remote -v";
|
||||
}
|
||||
|
||||
public List<Models.Remote> Result() {
|
||||
Exec();
|
||||
return loaded;
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_REMOTE.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
|
@ -28,8 +25,10 @@ namespace SourceGit.Commands {
|
|||
URL = match.Groups[2].Value,
|
||||
};
|
||||
|
||||
if (loaded.Find(x => x.Name == remote.Name) != null) return;
|
||||
loaded.Add(remote);
|
||||
if (_loaded.Find(x => x.Name == remote.Name) != null) return;
|
||||
_loaded.Add(remote);
|
||||
}
|
||||
|
||||
private List<Models.Remote> _loaded = new List<Models.Remote>();
|
||||
}
|
||||
}
|
15
src/Commands/QueryRepositoryRootPath.cs
Normal file
15
src/Commands/QueryRepositoryRootPath.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace SourceGit.Commands {
|
||||
public class QueryRepositoryRootPath : Command {
|
||||
public QueryRepositoryRootPath(string path) {
|
||||
WorkingDirectory = path;
|
||||
Args = "rev-parse --show-toplevel";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public string Result() {
|
||||
var rs = ReadToEnd().StdOut;
|
||||
if (string.IsNullOrEmpty(rs)) return null;
|
||||
return rs.Trim();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取出指定Revision下的文件列表
|
||||
/// </summary>
|
||||
public class RevisionObjects : Command {
|
||||
public class QueryRevisionObjects : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
|
||||
private List<Models.Object> objects = new List<Models.Object>();
|
||||
|
||||
public RevisionObjects(string cwd, string sha) {
|
||||
Cwd = cwd;
|
||||
public QueryRevisionObjects(string repo, string sha) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"ls-tree -r {sha}";
|
||||
}
|
||||
|
||||
|
@ -19,7 +17,7 @@ namespace SourceGit.Commands {
|
|||
return objects;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
37
src/Commands/QueryStashChanges.cs
Normal file
37
src/Commands/QueryStashChanges.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class QueryStashChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
|
||||
public QueryStashChanges(string repo, string sha) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return _changes;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
|
||||
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
|
||||
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
|
||||
case 'R': change.Set(Models.ChangeState.Renamed); _changes.Add(change); break;
|
||||
case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
|
@ -1,48 +1,47 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 解析当前仓库中的贮藏
|
||||
/// </summary>
|
||||
public class Stashes : Command {
|
||||
public class QueryStashes : Command {
|
||||
private static readonly Regex REG_STASH = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$");
|
||||
private List<Models.Stash> parsed = new List<Models.Stash>();
|
||||
private Models.Stash current = null;
|
||||
|
||||
public Stashes(string path) {
|
||||
Cwd = path;
|
||||
public QueryStashes(string repo) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "stash list --pretty=raw";
|
||||
}
|
||||
|
||||
public List<Models.Stash> Result() {
|
||||
Exec();
|
||||
if (current != null) parsed.Add(current);
|
||||
return parsed;
|
||||
if (_current != null) _stashes.Add(_current);
|
||||
return _stashes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
protected override void OnReadline(string line) {
|
||||
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
|
||||
if (current != null && !string.IsNullOrEmpty(current.Name)) parsed.Add(current);
|
||||
current = new Models.Stash() { SHA = line.Substring(7, 8) };
|
||||
if (_current != null && !string.IsNullOrEmpty(_current.Name)) _stashes.Add(_current);
|
||||
_current = new Models.Stash() { SHA = line.Substring(7, 8) };
|
||||
return;
|
||||
}
|
||||
|
||||
if (current == null) return;
|
||||
if (_current == null) return;
|
||||
|
||||
if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) {
|
||||
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)) {
|
||||
current.Message = line.Substring(16);
|
||||
_current.Message = line.Substring(16);
|
||||
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
|
||||
Models.User user = Models.User.Invalid;
|
||||
ulong time = 0;
|
||||
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
|
||||
current.Author = user;
|
||||
current.Time = time;
|
||||
_current.Author = user;
|
||||
_current.Time = time;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Models.Stash> _stashes = new List<Models.Stash>();
|
||||
private Models.Stash _current = null;
|
||||
}
|
||||
}
|
27
src/Commands/QuerySubmodules.cs
Normal file
27
src/Commands/QuerySubmodules.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class QuerySubmodules : Command {
|
||||
private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$");
|
||||
|
||||
public QuerySubmodules(string repo) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "submodule status";
|
||||
}
|
||||
|
||||
public List<string> Result() {
|
||||
Exec();
|
||||
return _submodules;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
_submodules.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
private List<string> _submodules = new List<string>();
|
||||
}
|
||||
}
|
34
src/Commands/QueryTags.cs
Normal file
34
src/Commands/QueryTags.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
public class QueryTags : Command {
|
||||
public QueryTags(string repo) {
|
||||
Context = repo;
|
||||
WorkingDirectory = repo;
|
||||
Args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
|
||||
}
|
||||
|
||||
public List<Models.Tag> Result() {
|
||||
Exec();
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line) {
|
||||
var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (subs.Length == 2) {
|
||||
_loaded.Add(new Models.Tag() {
|
||||
Name = subs[0],
|
||||
SHA = subs[1],
|
||||
});
|
||||
} else if (subs.Length == 3) {
|
||||
_loaded.Add(new Models.Tag() {
|
||||
Name = subs[0],
|
||||
SHA = subs[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private List<Models.Tag> _loaded = new List<Models.Tag>();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 变基命令
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class Rebase : Command {
|
||||
|
||||
public Rebase(string repo, string basedOn, bool autoStash) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "rebase ";
|
||||
if (autoStash) Args += "--autostash ";
|
||||
Args += basedOn;
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 远程操作
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class Remote : Command {
|
||||
|
||||
public Remote(string repo) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Add(string name, string url) {
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 重置命令
|
||||
/// </summary>
|
||||
public class Reset : Command {
|
||||
|
||||
public Reset(string repo) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "reset";
|
||||
}
|
||||
|
||||
public Reset(string repo, string revision, string mode) {
|
||||
Cwd = repo;
|
||||
Args = $"reset {mode} {revision}";
|
||||
}
|
||||
public Reset(string repo, List<Models.Change> changes) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
public Reset(string repo, List<string> files) {
|
||||
Cwd = repo;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("reset --");
|
||||
foreach (var f in files) {
|
||||
foreach (var c in changes) {
|
||||
builder.Append(" \"");
|
||||
builder.Append(f);
|
||||
builder.Append(c.Path);
|
||||
builder.Append("\"");
|
||||
}
|
||||
Args = builder.ToString();
|
||||
}
|
||||
|
||||
public Reset(string repo, string revision, string mode) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"reset {mode} {revision}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 撤销提交
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class Revert : Command {
|
||||
|
||||
public Revert(string repo, string commit, bool autoCommit) {
|
||||
Cwd = repo;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"revert {commit} --no-edit";
|
||||
if (!autoCommit) Args += " --no-commit";
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 编辑HEAD的提交信息
|
||||
/// </summary>
|
||||
public class Reword : Command {
|
||||
public Reword(string repo, string msg) {
|
||||
var tmp = Path.GetTempFileName();
|
||||
File.WriteAllText(tmp, msg);
|
||||
|
||||
Cwd = repo;
|
||||
Args = $"commit --amend --allow-empty --file=\"{tmp}\"";
|
||||
}
|
||||
}
|
||||
}
|
43
src/Commands/SaveChangesAsPatch.cs
Normal file
43
src/Commands/SaveChangesAsPatch.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer) {
|
||||
var starter = new ProcessStartInfo();
|
||||
starter.WorkingDirectory = repo;
|
||||
starter.FileName = Native.OS.GitExecutableFile;
|
||||
starter.Arguments = $"diff --ignore-cr-at-eol --unified=4 {opt}";
|
||||
starter.UseShellExecute = false;
|
||||
starter.CreateNoWindow = true;
|
||||
starter.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
starter.RedirectStandardOutput = true;
|
||||
|
||||
try {
|
||||
var proc = new Process() { StartInfo = starter };
|
||||
proc.Start();
|
||||
proc.StandardOutput.BaseStream.CopyTo(writer);
|
||||
proc.WaitForExit();
|
||||
var rs = proc.ExitCode == 0;
|
||||
proc.Close();
|
||||
|
||||
return rs;
|
||||
} catch (Exception e) {
|
||||
App.RaiseException(repo, "Save change to patch failed: " + e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 将Changes保存到文件流中
|
||||
/// </summary>
|
||||
public class SaveChangeToStream : Command {
|
||||
private StreamWriter writer = null;
|
||||
|
||||
public SaveChangeToStream(string repo, Models.Change change, StreamWriter to) {
|
||||
Cwd = repo;
|
||||
if (change.WorkTree == Models.Change.Status.Added || change.WorkTree == Models.Change.Status.Untracked) {
|
||||
Args = $"diff --no-index --no-ext-diff --find-renames -- /dev/null \"{change.Path}\"";
|
||||
} else {
|
||||
var pathspec = $"\"{change.Path}\"";
|
||||
if (!string.IsNullOrEmpty(change.OriginalPath)) pathspec = $"\"{change.OriginalPath}\" \"{change.Path}\"";
|
||||
Args = $"diff --binary --no-ext-diff --find-renames --full-index -- {pathspec}";
|
||||
}
|
||||
writer = to;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +1,60 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 保存指定版本的文件
|
||||
/// </summary>
|
||||
public class SaveRevisionFile {
|
||||
private string cwd = "";
|
||||
private string bat = "";
|
||||
|
||||
public SaveRevisionFile(string repo, string path, string sha, string saveTo) {
|
||||
var tmp = Path.GetTempFileName();
|
||||
var cmd = $"\"{Models.Preference.Instance.Git.Path}\" --no-pager ";
|
||||
|
||||
var isLFS = new LFS(repo).IsFiltered(path);
|
||||
if (isLFS) {
|
||||
cmd += $"show {sha}:\"{path}\" > {tmp}.lfs\n";
|
||||
cmd += $"\"{Models.Preference.Instance.Git.Path}\" --no-pager lfs smudge < {tmp}.lfs > \"{saveTo}\"\n";
|
||||
public static class SaveRevisionFile {
|
||||
public static void Run(string repo, string revision, string file, string saveTo) {
|
||||
var isLFSFiltered = new IsLFSFiltered(repo, file).Result();
|
||||
if (isLFSFiltered) {
|
||||
var tmpFile = saveTo + ".tmp";
|
||||
if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) {
|
||||
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
|
||||
}
|
||||
File.Delete(tmpFile);
|
||||
} else {
|
||||
cmd += $"show {sha}:\"{path}\" > \"{saveTo}\"\n";
|
||||
ExecCmd(repo, $"show {revision}:\"{file}\"", saveTo);
|
||||
}
|
||||
}
|
||||
|
||||
cwd = repo;
|
||||
bat = tmp + ".bat";
|
||||
|
||||
File.WriteAllText(bat, cmd);
|
||||
}
|
||||
|
||||
public void Exec() {
|
||||
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) {
|
||||
var starter = new ProcessStartInfo();
|
||||
starter.FileName = bat;
|
||||
starter.WorkingDirectory = cwd;
|
||||
starter.WorkingDirectory = repo;
|
||||
starter.FileName = Native.OS.GitExecutableFile;
|
||||
starter.Arguments = args;
|
||||
starter.UseShellExecute = false;
|
||||
starter.CreateNoWindow = true;
|
||||
starter.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
starter.RedirectStandardInput = true;
|
||||
starter.RedirectStandardOutput = true;
|
||||
starter.RedirectStandardError = true;
|
||||
|
||||
var proc = Process.Start(starter);
|
||||
using (var sw = File.OpenWrite(outputFile)) {
|
||||
try {
|
||||
var proc = new Process() { StartInfo = starter };
|
||||
proc.Start();
|
||||
|
||||
if (inputFile != null) {
|
||||
using (StreamReader sr = new StreamReader(inputFile)) {
|
||||
while (true) {
|
||||
var line = sr.ReadLine();
|
||||
if (line == null) break;
|
||||
proc.StandardInput.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proc.StandardOutput.BaseStream.CopyTo(sw);
|
||||
proc.WaitForExit();
|
||||
var rs = proc.ExitCode == 0;
|
||||
proc.Close();
|
||||
|
||||
File.Delete(bat);
|
||||
return rs;
|
||||
} catch (Exception e) {
|
||||
App.RaiseException(repo, "Save file failed: " + e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 单个贮藏相关操作
|
||||
/// </summary>
|
||||
public class Stash : Command {
|
||||
|
||||
public Stash(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Push(List<Models.Change> changes, string message, bool bFull) {
|
||||
if (bFull) {
|
||||
var needAdd = new List<string>();
|
||||
foreach (var c in changes) {
|
||||
if (c.WorkTree == Models.Change.Status.Added || c.WorkTree == Models.Change.Status.Untracked) {
|
||||
needAdd.Add(c.Path);
|
||||
if (needAdd.Count > 10) {
|
||||
new Add(Cwd, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needAdd.Count > 0) {
|
||||
new Add(Cwd, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Push(string message) {
|
||||
Args = $"stash push -m \"{message}\"";
|
||||
return Exec();
|
||||
} else {
|
||||
}
|
||||
|
||||
public bool Push(List<Models.Change> changes, string message) {
|
||||
var temp = Path.GetTempFileName();
|
||||
var stream = new FileStream(temp, FileMode.Create);
|
||||
var writer = new StreamWriter(stream);
|
||||
|
||||
var needAdd = new List<string>();
|
||||
var needAdd = new List<Models.Change>();
|
||||
foreach (var c in changes) {
|
||||
writer.WriteLine(c.Path);
|
||||
|
||||
if (c.WorkTree == Models.Change.Status.Added || c.WorkTree == Models.Change.Status.Untracked) {
|
||||
needAdd.Add(c.Path);
|
||||
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) {
|
||||
needAdd.Add(c);
|
||||
if (needAdd.Count > 10) {
|
||||
new Add(Cwd, needAdd).Exec();
|
||||
new Add(WorkingDirectory, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needAdd.Count > 0) {
|
||||
new Add(Cwd, needAdd).Exec();
|
||||
new Add(WorkingDirectory, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
|
||||
|
@ -63,7 +45,6 @@ namespace SourceGit.Commands {
|
|||
File.Delete(temp);
|
||||
return succ;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Apply(string name) {
|
||||
Args = $"stash apply -q {name}";
|
||||
|
@ -79,5 +60,10 @@ namespace SourceGit.Commands {
|
|||
Args = $"stash drop -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Clear() {
|
||||
Args = "stash clear";
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查看Stash中的修改
|
||||
/// </summary>
|
||||
public class StashChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public StashChanges(string repo, string sha) {
|
||||
Cwd = repo;
|
||||
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
|
||||
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
|
||||
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
|
||||
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
|
||||
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 子树相关操作
|
||||
/// </summary>
|
||||
public class SubTree : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public SubTree(string repo) {
|
||||
Cwd = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
|
||||
public bool Add(string prefix, string source, string revision, bool squash, Action<string> onProgress) {
|
||||
var path = Path.Combine(Cwd, prefix);
|
||||
if (Directory.Exists(path)) return true;
|
||||
|
||||
handler = onProgress;
|
||||
Args = $"subtree add --prefix=\"{prefix}\" {source} {revision}";
|
||||
if (squash) Args += " --squash";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public void Pull(string prefix, string source, string branch, bool squash, Action<string> onProgress) {
|
||||
handler = onProgress;
|
||||
Args = $"subtree pull --prefix=\"{prefix}\" {source} {branch}";
|
||||
if (squash) Args += " --squash";
|
||||
Exec();
|
||||
}
|
||||
|
||||
public void Push(string prefix, string source, string branch, Action<string> onProgress) {
|
||||
handler = onProgress;
|
||||
Args = $"subtree push --prefix=\"{prefix}\" {source} {branch}";
|
||||
Exec();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +1,22 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 子模块
|
||||
/// </summary>
|
||||
public class Submodule : Command {
|
||||
private Action<string> onProgress = null;
|
||||
|
||||
public Submodule(string cwd) {
|
||||
Cwd = cwd;
|
||||
public Submodule(string repo) {
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Add(string url, string path, bool recursive, Action<string> handler) {
|
||||
Args = $"submodule add {url} {path}";
|
||||
onProgress = handler;
|
||||
public bool Add(string url, string relativePath, bool recursive, Action<string> outputHandler) {
|
||||
_outputHandler = outputHandler;
|
||||
Args = $"submodule add {url} {relativePath}";
|
||||
if (!Exec()) return false;
|
||||
|
||||
if (recursive) {
|
||||
Args = $"submodule update --init --recursive -- {path}";
|
||||
Args = $"submodule update --init --recursive -- {relativePath}";
|
||||
return Exec();
|
||||
} else {
|
||||
Args = $"submodule update --init -- {relativePath}";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -29,16 +26,18 @@ namespace SourceGit.Commands {
|
|||
return Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string path) {
|
||||
Args = $"submodule deinit -f {path}";
|
||||
public bool Delete(string relativePath) {
|
||||
Args = $"submodule deinit -f {relativePath}";
|
||||
if (!Exec()) return false;
|
||||
|
||||
Args = $"rm -rf {path}";
|
||||
Args = $"rm -rf {relativePath}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
onProgress?.Invoke(line);
|
||||
protected override void OnReadline(string line) {
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 获取子模块列表
|
||||
/// </summary>
|
||||
public class Submodules : Command {
|
||||
private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$");
|
||||
private List<string> modules = new List<string>();
|
||||
|
||||
public Submodules(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "submodule status";
|
||||
}
|
||||
|
||||
public List<string> Result() {
|
||||
Exec();
|
||||
return modules;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
modules.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 标签相关指令
|
||||
/// </summary>
|
||||
public class Tag : Command {
|
||||
|
||||
public Tag(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Add(string name, string basedOn, string message) {
|
||||
Args = $"tag -a {name} {basedOn} ";
|
||||
public static class Tag {
|
||||
public static bool Add(string repo, string name, string basedOn, string message) {
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
cmd.Args = $"tag -a {name} {basedOn} ";
|
||||
|
||||
if (!string.IsNullOrEmpty(message)) {
|
||||
string tmp = Path.GetTempFileName();
|
||||
File.WriteAllText(tmp, message);
|
||||
Args += $"-F \"{tmp}\"";
|
||||
cmd.Args += $"-F \"{tmp}\"";
|
||||
} else {
|
||||
Args += $"-m {name}";
|
||||
cmd.Args += $"-m {name}";
|
||||
}
|
||||
|
||||
return Exec();
|
||||
return cmd.Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string name, bool push) {
|
||||
Args = $"tag --delete {name}";
|
||||
if (!Exec()) return false;
|
||||
public static bool Delete(string repo, string name, List<Models.Remote> remotes) {
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
cmd.Args = $"tag --delete {name}";
|
||||
if (!cmd.Exec()) return false;
|
||||
|
||||
if (push) {
|
||||
var remotes = new Remotes(Cwd).Result();
|
||||
if (remotes != null) {
|
||||
foreach (var r in remotes) {
|
||||
new Push(Cwd, r.Name, name, true).Exec();
|
||||
new Push(repo, r.Name, name, true).Exec();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 解析所有的Tags
|
||||
/// </summary>
|
||||
public class Tags : Command {
|
||||
public static readonly string CMD = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
|
||||
private List<Models.Tag> loaded = new List<Models.Tag>();
|
||||
|
||||
public Tags(string path) {
|
||||
Cwd = path;
|
||||
Args = CMD;
|
||||
}
|
||||
|
||||
public List<Models.Tag> Result() {
|
||||
Exec();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (subs.Length == 2) {
|
||||
loaded.Add(new Models.Tag() {
|
||||
Name = subs[0],
|
||||
SHA = subs[1],
|
||||
});
|
||||
} else if (subs.Length == 3) {
|
||||
loaded.Add(new Models.Tag() {
|
||||
Name = subs[0],
|
||||
SHA = subs[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,14 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 检测git是否可用,并获取git版本信息
|
||||
/// </summary>
|
||||
namespace SourceGit.Commands {
|
||||
public class Version : Command {
|
||||
const string GitVersionPrefix = "git version ";
|
||||
public Version() {
|
||||
Args = "-v";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public string Query() {
|
||||
Args = $"--version";
|
||||
var result = ReadToEnd();
|
||||
if (!result.IsSuccess || string.IsNullOrEmpty(result.Output)) return null;
|
||||
var version = result.Output.Trim();
|
||||
if (!version.StartsWith(GitVersionPrefix, StringComparison.Ordinal)) return null;
|
||||
return version.Substring(GitVersionPrefix.Length);
|
||||
var rs = ReadToEnd();
|
||||
if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) return string.Empty;
|
||||
return rs.StdOut.Trim().Substring("git version ".Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
src/Converters/BookmarkConverters.cs
Normal file
12
src/Converters/BookmarkConverters.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class BookmarkConverters {
|
||||
public static FuncValueConverter<int, IBrush> ToBrush =
|
||||
new FuncValueConverter<int, IBrush>(bookmark => Models.Bookmarks.Brushes[bookmark]);
|
||||
|
||||
public static FuncValueConverter<int, double> ToStrokeThickness =
|
||||
new FuncValueConverter<int, double>(bookmark => bookmark == 0 ? 1.0 : 0);
|
||||
}
|
||||
}
|
8
src/Converters/BoolConverters.cs
Normal file
8
src/Converters/BoolConverters.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class BoolConverters {
|
||||
public static FuncValueConverter<bool, double> ToCommitOpacity =
|
||||
new FuncValueConverter<bool, double>(x => x ? 1 : 0.5);
|
||||
}
|
||||
}
|
8
src/Converters/BranchConverters.cs
Normal file
8
src/Converters/BranchConverters.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class BranchConverters {
|
||||
public static FuncValueConverter<Models.Branch, string> ToName =
|
||||
new FuncValueConverter<Models.Branch, string>(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}");
|
||||
}
|
||||
}
|
28
src/Converters/ChangeViewModeConverters.cs
Normal file
28
src/Converters/ChangeViewModeConverters.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class ChangeViewModeConverters {
|
||||
public static FuncValueConverter<Models.ChangeViewMode, StreamGeometry> ToIcon =
|
||||
new FuncValueConverter<Models.ChangeViewMode, StreamGeometry>(v => {
|
||||
switch (v) {
|
||||
case Models.ChangeViewMode.List:
|
||||
return App.Current?.FindResource("Icons.List") as StreamGeometry;
|
||||
case Models.ChangeViewMode.Grid:
|
||||
return App.Current?.FindResource("Icons.Grid") as StreamGeometry;
|
||||
default:
|
||||
return App.Current?.FindResource("Icons.Tree") as StreamGeometry;
|
||||
}
|
||||
});
|
||||
|
||||
public static FuncValueConverter<Models.ChangeViewMode, bool> IsList =
|
||||
new FuncValueConverter<Models.ChangeViewMode, bool>(v => v == Models.ChangeViewMode.List);
|
||||
|
||||
public static FuncValueConverter<Models.ChangeViewMode, bool> IsGrid =
|
||||
new FuncValueConverter<Models.ChangeViewMode, bool>(v => v == Models.ChangeViewMode.Grid);
|
||||
|
||||
public static FuncValueConverter<Models.ChangeViewMode, bool> IsTree =
|
||||
new FuncValueConverter<Models.ChangeViewMode, bool>(v => v == Models.ChangeViewMode.Tree);
|
||||
}
|
||||
}
|
34
src/Converters/DecoratorTypeConverters.cs
Normal file
34
src/Converters/DecoratorTypeConverters.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class DecoratorTypeConverters {
|
||||
public static FuncValueConverter<Models.DecoratorType, IBrush> ToBackground =
|
||||
new FuncValueConverter<Models.DecoratorType, IBrush>(v => {
|
||||
if (v == Models.DecoratorType.Tag) return Models.DecoratorResources.Backgrounds[0];
|
||||
return Models.DecoratorResources.Backgrounds[1];
|
||||
});
|
||||
|
||||
public static FuncValueConverter<Models.DecoratorType, StreamGeometry> ToIcon =
|
||||
new FuncValueConverter<Models.DecoratorType, StreamGeometry>(v => {
|
||||
var key = "Icons.Tag";
|
||||
switch (v) {
|
||||
case Models.DecoratorType.CurrentBranchHead:
|
||||
key = "Icons.Check";
|
||||
break;
|
||||
case Models.DecoratorType.RemoteBranchHead:
|
||||
key = "Icons.Remote";
|
||||
break;
|
||||
case Models.DecoratorType.LocalBranchHead:
|
||||
key = "Icons.Branch";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return Application.Current?.FindResource(key) as StreamGeometry;
|
||||
});
|
||||
}
|
||||
}
|
27
src/Converters/IntConverters.cs
Normal file
27
src/Converters/IntConverters.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class IntConverters {
|
||||
public static FuncValueConverter<int, bool> IsGreaterThanZero =
|
||||
new FuncValueConverter<int, bool>(v => v > 0);
|
||||
|
||||
public static FuncValueConverter<int, bool> IsZero =
|
||||
new FuncValueConverter<int, bool>(v => v == 0);
|
||||
|
||||
public class NotEqualConverter : IValueConverter {
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
int v = (int)value;
|
||||
int target = (int)parameter;
|
||||
return v != target;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public static NotEqualConverter NotEqual = new NotEqualConverter();
|
||||
}
|
||||
}
|
28
src/Converters/LauncherPageConverters.cs
Normal file
28
src/Converters/LauncherPageConverters.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Data.Converters;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class LauncherPageConverters {
|
||||
public static FuncMultiValueConverter<object, bool> ToTabSeperatorVisible =
|
||||
new FuncMultiValueConverter<object, bool>(v => {
|
||||
if (v == null) return false;
|
||||
|
||||
var array = new List<object>();
|
||||
array.AddRange(v);
|
||||
if (array.Count != 3) return false;
|
||||
|
||||
var self = array[0] as ViewModels.LauncherPage;
|
||||
if (self == null) return false;
|
||||
|
||||
var selected = array[1] as ViewModels.LauncherPage;
|
||||
var collections = array[2] as AvaloniaList<ViewModels.LauncherPage>;
|
||||
|
||||
if (selected != null && collections != null && (self == selected || collections.IndexOf(self) + 1 == collections.IndexOf(selected))) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
12
src/Converters/ListConverters.cs
Normal file
12
src/Converters/ListConverters.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using System.Collections;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class ListConverters {
|
||||
public static FuncValueConverter<IList, string> ToCount =
|
||||
new FuncValueConverter<IList, string>(v => $" ({v.Count})");
|
||||
|
||||
public static FuncValueConverter<IList, bool> IsNotNullOrEmpty =
|
||||
new FuncValueConverter<IList, bool>(v => v != null && v.Count > 0);
|
||||
}
|
||||
}
|
18
src/Converters/PathConverters.cs
Normal file
18
src/Converters/PathConverters.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class PathConverters {
|
||||
public static FuncValueConverter<string, string> PureFileName =
|
||||
new FuncValueConverter<string, string>(fullpath => Path.GetFileName(fullpath) ?? "");
|
||||
|
||||
public static FuncValueConverter<string, string> PureDirectoryName =
|
||||
new FuncValueConverter<string, string>(fullpath => Path.GetDirectoryName(fullpath) ?? "");
|
||||
|
||||
public static FuncValueConverter<string, string> TruncateIfTooLong =
|
||||
new FuncValueConverter<string, string>(fullpath => {
|
||||
if (fullpath.Length <= 50) return fullpath;
|
||||
return fullpath.Substring(0, 20) + ".../" + Path.GetFileName(fullpath);
|
||||
});
|
||||
}
|
||||
}
|
56
src/Converters/StringConverters.cs
Normal file
56
src/Converters/StringConverters.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return (value as Models.Locale).Key;
|
||||
}
|
||||
}
|
||||
|
||||
public static ToLocaleConverter ToLocale = new ToLocaleConverter();
|
||||
|
||||
public class ToThemeConverter : IValueConverter {
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var theme = (string)value;
|
||||
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) {
|
||||
return ThemeVariant.Light;
|
||||
} else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) {
|
||||
return ThemeVariant.Dark;
|
||||
} else {
|
||||
return ThemeVariant.Default;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var theme = (ThemeVariant)value;
|
||||
return theme.Key;
|
||||
}
|
||||
}
|
||||
|
||||
public static ToThemeConverter ToTheme = new ToThemeConverter();
|
||||
|
||||
public class FormatByResourceKeyConverter : IValueConverter {
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var key = parameter as string;
|
||||
return App.Text(key, value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public static FormatByResourceKeyConverter FormatByResourceKey = new FormatByResourceKeyConverter();
|
||||
|
||||
public static FuncValueConverter<string, string> ToShortSHA =
|
||||
new FuncValueConverter<string, string>(v => v.Length > 10 ? v.Substring(0, 10) : v);
|
||||
}
|
||||
}
|
36
src/Converters/WindowStateConverters.cs
Normal file
36
src/Converters/WindowStateConverters.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
public static class WindowStateConverters {
|
||||
public static FuncValueConverter<WindowState, Thickness> ToContentMargin =
|
||||
new FuncValueConverter<WindowState, Thickness>(state => {
|
||||
if (state == WindowState.Maximized && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
return new Thickness(6);
|
||||
} else {
|
||||
return new Thickness(0);
|
||||
}
|
||||
});
|
||||
|
||||
public static FuncValueConverter<WindowState, GridLength> ToTitleBarHeight =
|
||||
new FuncValueConverter<WindowState, GridLength>(state => {
|
||||
if (state == WindowState.Maximized && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
return new GridLength(30);
|
||||
} else {
|
||||
return new GridLength(38);
|
||||
}
|
||||
});
|
||||
|
||||
public static FuncValueConverter<WindowState, StreamGeometry> ToMaxOrRestoreIcon =
|
||||
new FuncValueConverter<WindowState, StreamGeometry>(state => {
|
||||
if (state == WindowState.Maximized) {
|
||||
return Application.Current?.FindResource("Icons.Window.Restore") as StreamGeometry;
|
||||
} else {
|
||||
return Application.Current?.FindResource("Icons.Window.Maximize") as StreamGeometry;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura />
|
||||
</Weavers>
|
|
@ -1,141 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCompression" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
13
src/Models/ApplyWhiteSpaceMode.cs
Normal file
13
src/Models/ApplyWhiteSpaceMode.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Models {
|
||||
public class ApplyWhiteSpaceMode {
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
|
||||
public ApplyWhiteSpaceMode(string n, string d, string a) {
|
||||
Name = App.Text(n);
|
||||
Desc = App.Text(d);
|
||||
Arg = a;
|
||||
}
|
||||
}
|
||||
}
|
120
src/Models/AvatarManager.cs
Normal file
120
src/Models/AvatarManager.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public interface IAvatarHost {
|
||||
void OnAvatarResourceReady(string md5, Bitmap bitmap);
|
||||
}
|
||||
|
||||
public static class AvatarManager {
|
||||
static AvatarManager() {
|
||||
_storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit", "avatars");
|
||||
if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath);
|
||||
|
||||
Task.Run(() => {
|
||||
while (true) {
|
||||
var md5 = null as string;
|
||||
|
||||
lock (_synclock) {
|
||||
foreach (var one in _requesting) {
|
||||
md5 = one;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (md5 == null) {
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
var localFile = Path.Combine(_storePath, md5);
|
||||
var img = null as Bitmap;
|
||||
try {
|
||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) };
|
||||
var task = client.GetAsync($"https://cravatar.cn/avatar/{md5}?d=404");
|
||||
task.Wait();
|
||||
|
||||
var rsp = task.Result;
|
||||
if (rsp.IsSuccessStatusCode) {
|
||||
using (var stream = rsp.Content.ReadAsStream()) {
|
||||
using (var writer = File.OpenWrite(localFile)) {
|
||||
stream.CopyTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
using (var reader = File.OpenRead(localFile)) {
|
||||
img = Bitmap.DecodeToWidth(reader, 128);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
lock (_synclock) {
|
||||
_requesting.Remove(md5);
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() => {
|
||||
if (_resources.ContainsKey(md5)) _resources[md5] = img;
|
||||
else _resources.Add(md5, img);
|
||||
if (img != null) NotifyResourceReady(md5, img);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void Subscribe(IAvatarHost host) {
|
||||
_avatars.Add(new WeakReference<IAvatarHost>(host));
|
||||
}
|
||||
|
||||
public static Bitmap Request(string md5, bool forceRefetch = false) {
|
||||
if (forceRefetch) {
|
||||
if (_resources.ContainsKey(md5)) _resources.Remove(md5);
|
||||
} else {
|
||||
if (_resources.ContainsKey(md5)) return _resources[md5];
|
||||
|
||||
var localFile = Path.Combine(_storePath, md5);
|
||||
if (File.Exists(localFile)) {
|
||||
try {
|
||||
using (var stream = File.OpenRead(localFile)) {
|
||||
var img = Bitmap.DecodeToWidth(stream, 128);
|
||||
_resources.Add(md5, img);
|
||||
return img;
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
lock (_synclock) {
|
||||
if (!_requesting.Contains(md5)) _requesting.Add(md5);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void NotifyResourceReady(string md5, Bitmap bitmap) {
|
||||
List<WeakReference<IAvatarHost>> invalids = new List<WeakReference<IAvatarHost>>();
|
||||
foreach (var avatar in _avatars) {
|
||||
IAvatarHost retrived = null;
|
||||
if (avatar.TryGetTarget(out retrived)) {
|
||||
retrived.OnAvatarResourceReady(md5, bitmap);
|
||||
break;
|
||||
} else {
|
||||
invalids.Add(avatar);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var invalid in invalids) _avatars.Remove(invalid);
|
||||
}
|
||||
|
||||
private static object _synclock = new object();
|
||||
private static string _storePath = string.Empty;
|
||||
private static List<WeakReference<IAvatarHost>> _avatars = new List<WeakReference<IAvatarHost>>();
|
||||
private static Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
|
||||
private static HashSet<string> _requesting = new HashSet<string>();
|
||||
}
|
||||
}
|
17
src/Models/Blame.cs
Normal file
17
src/Models/Blame.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class BlameLineInfo {
|
||||
public bool IsFirstInGroup { get; set; } = false;
|
||||
public string CommitSHA { get; set; } = string.Empty;
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string Time { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BlameData {
|
||||
public string File { get; set; } = string.Empty;
|
||||
public List<BlameLineInfo> LineInfos { get; set; } = new List<BlameLineInfo>();
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsBinary { get; set; } = false;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 追溯中的行信息
|
||||
/// </summary>
|
||||
public class BlameLine {
|
||||
public string LineNumber { get; set; }
|
||||
public string CommitSHA { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Time { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
}
|
22
src/Models/Bookmarks.cs
Normal file
22
src/Models/Bookmarks.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public static class Bookmarks {
|
||||
public static readonly Avalonia.Media.IBrush[] Brushes = [
|
||||
Avalonia.Media.Brushes.Transparent,
|
||||
Avalonia.Media.Brushes.Red,
|
||||
Avalonia.Media.Brushes.Orange,
|
||||
Avalonia.Media.Brushes.Gold,
|
||||
Avalonia.Media.Brushes.ForestGreen,
|
||||
Avalonia.Media.Brushes.DarkCyan,
|
||||
Avalonia.Media.Brushes.DeepSkyBlue,
|
||||
Avalonia.Media.Brushes.Purple,
|
||||
];
|
||||
|
||||
public static readonly List<int> Supported = new List<int>();
|
||||
|
||||
static Bookmarks() {
|
||||
for (int i = 0; i < Brushes.Length; i++) Supported.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 分支数据
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public class Branch {
|
||||
public string Name { get; set; }
|
||||
public string FullName { get; set; }
|
||||
|
|
152
src/Models/BranchTreeNode.cs
Normal file
152
src/Models/BranchTreeNode.cs
Normal file
|
@ -0,0 +1,152 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public enum BranchTreeNodeType {
|
||||
Remote,
|
||||
Folder,
|
||||
Branch,
|
||||
}
|
||||
|
||||
public class BranchTreeNode {
|
||||
public string Name { get; set; }
|
||||
public BranchTreeNodeType Type { get; set; }
|
||||
public object Backend { get; set; }
|
||||
public bool IsExpanded { get; set; }
|
||||
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
|
||||
|
||||
public bool IsUpstreamTrackStatusVisible {
|
||||
get => IsBranch && !string.IsNullOrEmpty((Backend as Branch).UpstreamTrackStatus);
|
||||
}
|
||||
|
||||
public string UpstreamTrackStatus {
|
||||
get => Type == BranchTreeNodeType.Branch ? (Backend as Branch).UpstreamTrackStatus : "";
|
||||
}
|
||||
|
||||
public bool IsRemote {
|
||||
get => Type == BranchTreeNodeType.Remote;
|
||||
}
|
||||
|
||||
public bool IsFolder {
|
||||
get => Type == BranchTreeNodeType.Folder;
|
||||
}
|
||||
|
||||
public bool IsBranch {
|
||||
get => Type == BranchTreeNodeType.Branch;
|
||||
}
|
||||
|
||||
public bool IsCurrent {
|
||||
get => IsBranch && (Backend as Branch).IsCurrent;
|
||||
}
|
||||
|
||||
public class Builder {
|
||||
public List<BranchTreeNode> Locals => _locals;
|
||||
public List<BranchTreeNode> Remotes => _remotes;
|
||||
|
||||
public void Run(List<Branch> branches, List<Remote> remotes) {
|
||||
foreach (var remote in remotes) {
|
||||
var path = $"remote/{remote.Name}";
|
||||
var node = new BranchTreeNode() {
|
||||
Name = remote.Name,
|
||||
Type = BranchTreeNodeType.Remote,
|
||||
Backend = remote,
|
||||
IsExpanded = _expanded.Contains(path),
|
||||
};
|
||||
|
||||
_maps.Add(path, node);
|
||||
_remotes.Add(node);
|
||||
}
|
||||
|
||||
foreach (var branch in branches) {
|
||||
if (branch.IsLocal) {
|
||||
MakeBranchNode(branch, _locals, "local");
|
||||
} else {
|
||||
var remote = _remotes.Find(x => x.Name == branch.Remote);
|
||||
if (remote != null) MakeBranchNode(branch, remote.Children, $"remote/{remote.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
SortNodes(_locals);
|
||||
SortNodes(_remotes);
|
||||
}
|
||||
|
||||
public void CollectExpandedNodes(List<BranchTreeNode> nodes, bool isLocal) {
|
||||
CollectExpandedNodes(nodes, isLocal ? "local" : "remote");
|
||||
}
|
||||
|
||||
private void CollectExpandedNodes(List<BranchTreeNode> nodes, string prefix) {
|
||||
foreach (var node in nodes) {
|
||||
var path = prefix + "/" + node.Name;
|
||||
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) _expanded.Add(path);
|
||||
CollectExpandedNodes(node.Children, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void MakeBranchNode(Branch branch, List<BranchTreeNode> roots, string prefix) {
|
||||
var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (subs.Length == 1) {
|
||||
var node = new BranchTreeNode() {
|
||||
Name = subs[0],
|
||||
Type = BranchTreeNodeType.Branch,
|
||||
Backend = branch,
|
||||
IsExpanded = false,
|
||||
};
|
||||
roots.Add(node);
|
||||
return;
|
||||
}
|
||||
|
||||
BranchTreeNode lastFolder = null;
|
||||
string path = prefix;
|
||||
for (int i = 0; i < subs.Length - 1; i++) {
|
||||
path = string.Concat(path, "/", subs[i]);
|
||||
if (_maps.ContainsKey(path)) {
|
||||
lastFolder = _maps[path];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new BranchTreeNode() {
|
||||
Name = subs[i],
|
||||
Type = BranchTreeNodeType.Folder,
|
||||
IsExpanded = _expanded.Contains(path),
|
||||
};
|
||||
roots.Add(lastFolder);
|
||||
_maps.Add(path, lastFolder);
|
||||
} else {
|
||||
var folder = new BranchTreeNode() {
|
||||
Name = subs[i],
|
||||
Type = BranchTreeNodeType.Folder,
|
||||
IsExpanded = _expanded.Contains(path),
|
||||
};
|
||||
_maps.Add(path, folder);
|
||||
lastFolder.Children.Add(folder);
|
||||
lastFolder = folder;
|
||||
}
|
||||
}
|
||||
|
||||
var last = new BranchTreeNode() {
|
||||
Name = subs[subs.Length - 1],
|
||||
Type = BranchTreeNodeType.Branch,
|
||||
Backend = branch,
|
||||
IsExpanded = false,
|
||||
};
|
||||
lastFolder.Children.Add(last);
|
||||
}
|
||||
|
||||
private void SortNodes(List<BranchTreeNode> nodes) {
|
||||
nodes.Sort((l, r) => {
|
||||
if (l.Type == r.Type) {
|
||||
return l.Name.CompareTo(r.Name);
|
||||
} else {
|
||||
return (int)(l.Type) - (int)(r.Type);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var node in nodes) SortNodes(node.Children);
|
||||
}
|
||||
|
||||
private List<BranchTreeNode> _locals = new List<BranchTreeNode>();
|
||||
private List<BranchTreeNode> _remotes = new List<BranchTreeNode>();
|
||||
private HashSet<string> _expanded = new HashSet<string>();
|
||||
private Dictionary<string, BranchTreeNode> _maps = new Dictionary<string, BranchTreeNode>();
|
||||
}
|
||||
}
|
||||
}
|
21
src/Models/CRLFMode.cs
Normal file
21
src/Models/CRLFMode.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class CRLFMode {
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Desc { get; set; }
|
||||
|
||||
public static List<CRLFMode> Supported = new List<CRLFMode>() {
|
||||
new CRLFMode("TRUE", "true", "Commit as LF, checkout as CRLF"),
|
||||
new CRLFMode("INPUT", "input", "Only convert for commit"),
|
||||
new CRLFMode("FALSE", "false", "Do NOT convert"),
|
||||
};
|
||||
|
||||
public CRLFMode(string name, string value, string desc) {
|
||||
Name = name;
|
||||
Value = value;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 自动换行处理方式
|
||||
/// </summary>
|
||||
public class CRLFOption {
|
||||
public string Display { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Desc { get; set; }
|
||||
|
||||
public static List<CRLFOption> Supported = new List<CRLFOption>() {
|
||||
new CRLFOption("TRUE", "true", "Commit as LF, checkout as CRLF"),
|
||||
new CRLFOption("INPUT", "input", "Only convert for commit"),
|
||||
new CRLFOption("FALSE", "false", "Do NOT convert"),
|
||||
};
|
||||
|
||||
public CRLFOption(string display, string value, string desc) {
|
||||
Display = display;
|
||||
Value = value;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,11 @@
|
|||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// Git变更
|
||||
/// </summary>
|
||||
public class Change {
|
||||
|
||||
/// <summary>
|
||||
/// 显示模式
|
||||
/// </summary>
|
||||
public enum DisplayMode {
|
||||
Tree,
|
||||
namespace SourceGit.Models {
|
||||
public enum ChangeViewMode {
|
||||
List,
|
||||
Grid,
|
||||
Tree,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更状态码
|
||||
/// </summary>
|
||||
public enum Status {
|
||||
public enum ChangeState {
|
||||
None,
|
||||
Modified,
|
||||
Added,
|
||||
|
@ -25,28 +13,29 @@ namespace SourceGit.Models {
|
|||
Renamed,
|
||||
Copied,
|
||||
Unmerged,
|
||||
Untracked,
|
||||
Untracked
|
||||
}
|
||||
|
||||
public Status Index { get; set; }
|
||||
public Status WorkTree { get; set; } = Status.None;
|
||||
public class Change {
|
||||
public ChangeState Index { get; set; }
|
||||
public ChangeState WorkTree { get; set; } = ChangeState.None;
|
||||
public string Path { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
|
||||
public bool IsConflit {
|
||||
get {
|
||||
if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true;
|
||||
if (Index == Status.Added && WorkTree == Status.Added) return true;
|
||||
if (Index == Status.Deleted && WorkTree == Status.Deleted) return true;
|
||||
if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged) return true;
|
||||
if (Index == ChangeState.Added && WorkTree == ChangeState.Added) return true;
|
||||
if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Set(Status index, Status workTree = Status.None) {
|
||||
public void Set(ChangeState index, ChangeState workTree = ChangeState.None) {
|
||||
Index = index;
|
||||
WorkTree = workTree;
|
||||
|
||||
if (index == Status.Renamed || workTree == Status.Renamed) {
|
||||
if (index == ChangeState.Renamed || workTree == ChangeState.Renamed) {
|
||||
var idx = Path.IndexOf('\t');
|
||||
if (idx >= 0) {
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
using Avalonia;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 提交记录
|
||||
/// </summary>
|
||||
public class Commit {
|
||||
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
public string SHA { get; set; } = string.Empty;
|
||||
public string ShortSHA => SHA.Substring(0, 8);
|
||||
public User Author { get; set; } = User.Invalid;
|
||||
public ulong AuthorTime { get; set; } = 0;
|
||||
public User Committer { get; set; } = User.Invalid;
|
||||
|
@ -23,10 +17,18 @@ namespace SourceGit.Models {
|
|||
public bool IsMerged { get; set; } = false;
|
||||
public Thickness Margin { get; set; } = new Thickness(0);
|
||||
|
||||
public string AuthorTimeStr => UTC_START.AddSeconds(AuthorTime).ToString("yyyy-MM-dd HH:mm:ss");
|
||||
public string CommitterTimeStr => UTC_START.AddSeconds(CommitterTime).ToString("yyyy-MM-dd HH:mm:ss");
|
||||
public string AuthorTimeShortStr => UTC_START.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
|
||||
public string CommitterTimeShortStr => UTC_START.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
|
||||
public string AuthorTimeStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string CommitterTimeStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
|
||||
public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
|
||||
|
||||
public bool IsCommitterVisible {
|
||||
get => Author != Committer || AuthorTime != CommitterTime;
|
||||
}
|
||||
|
||||
public string FullMessage {
|
||||
get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}";
|
||||
}
|
||||
|
||||
public static void ParseUserAndTime(string data, ref User user, ref ulong time) {
|
||||
var userEndIdx = data.IndexOf('>');
|
||||
|
@ -36,5 +38,7 @@ namespace SourceGit.Models {
|
|||
user = User.FindOrAdd(data.Substring(0, userEndIdx));
|
||||
time = timeEndIdx < 0 ? 0 : ulong.Parse(data.Substring(userEndIdx + 2, timeEndIdx - userEndIdx - 2));
|
||||
}
|
||||
|
||||
private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
}
|
||||
}
|
||||
|
|
205
src/Models/CommitGraph.cs
Normal file
205
src/Models/CommitGraph.cs
Normal file
|
@ -0,0 +1,205 @@
|
|||
using Avalonia;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class CommitGraph {
|
||||
public class Path {
|
||||
public List<Point> Points = new List<Point>();
|
||||
public int Color = 0;
|
||||
}
|
||||
|
||||
public class PathHelper {
|
||||
public string Next;
|
||||
public bool IsMerged;
|
||||
public double LastX;
|
||||
public double LastY;
|
||||
public double EndY;
|
||||
public Path Path;
|
||||
|
||||
public PathHelper(string next, bool isMerged, int color, Point start) {
|
||||
Next = next;
|
||||
IsMerged = isMerged;
|
||||
LastX = start.X;
|
||||
LastY = start.Y;
|
||||
EndY = LastY;
|
||||
|
||||
Path = new Path();
|
||||
Path.Color = color;
|
||||
Path.Points.Add(start);
|
||||
}
|
||||
|
||||
public PathHelper(string next, bool isMerged, int color, Point start, Point to) {
|
||||
Next = next;
|
||||
IsMerged = isMerged;
|
||||
LastX = to.X;
|
||||
LastY = to.Y;
|
||||
EndY = LastY;
|
||||
|
||||
Path = new Path();
|
||||
Path.Color = color;
|
||||
Path.Points.Add(start);
|
||||
Path.Points.Add(to);
|
||||
}
|
||||
|
||||
public void Add(double x, double y, double halfHeight, bool isEnd = false) {
|
||||
if (x > LastX) {
|
||||
Add(new Point(LastX, LastY));
|
||||
Add(new Point(x, y - halfHeight));
|
||||
if (isEnd) Add(new Point(x, y));
|
||||
} else if (x < LastX) {
|
||||
if (y > LastY + halfHeight) Add(new Point(LastX, LastY + halfHeight));
|
||||
Add(new Point(x, y));
|
||||
} else if (isEnd) {
|
||||
Add(new Point(x, y));
|
||||
}
|
||||
|
||||
LastX = x;
|
||||
LastY = y;
|
||||
}
|
||||
|
||||
private void Add(Point p) {
|
||||
if (EndY < p.Y) {
|
||||
Path.Points.Add(p);
|
||||
EndY = p.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Link {
|
||||
public Point Start;
|
||||
public Point Control;
|
||||
public Point End;
|
||||
public int Color;
|
||||
}
|
||||
|
||||
public class Dot {
|
||||
public Point Center;
|
||||
public int Color;
|
||||
}
|
||||
|
||||
public List<Path> Paths { get; set; } = new List<Path>();
|
||||
public List<Link> Links { get; set; } = new List<Link>();
|
||||
public List<Dot> Dots { get; set; } = new List<Dot>();
|
||||
|
||||
public static CommitGraph Parse(List<Commit> commits, double rowHeight, int colorCount) {
|
||||
double UNIT_WIDTH = 12;
|
||||
double HALF_WIDTH = 6;
|
||||
double UNIT_HEIGHT = rowHeight;
|
||||
double HALF_HEIGHT = rowHeight / 2;
|
||||
|
||||
var temp = new CommitGraph();
|
||||
var unsolved = new List<PathHelper>();
|
||||
var mapUnsolved = new Dictionary<string, PathHelper>();
|
||||
var ended = new List<PathHelper>();
|
||||
var offsetY = -HALF_HEIGHT;
|
||||
var colorIdx = 0;
|
||||
|
||||
foreach (var commit in commits) {
|
||||
var major = null as PathHelper;
|
||||
var isMerged = commit.IsMerged;
|
||||
var oldCount = unsolved.Count;
|
||||
|
||||
// Update current y offset
|
||||
offsetY += UNIT_HEIGHT;
|
||||
|
||||
// Find first curves that links to this commit and marks others that links to this commit ended.
|
||||
double offsetX = -HALF_WIDTH;
|
||||
foreach (var l in unsolved) {
|
||||
if (l.Next == commit.SHA) {
|
||||
if (major == null) {
|
||||
offsetX += UNIT_WIDTH;
|
||||
major = l;
|
||||
|
||||
if (commit.Parents.Count > 0) {
|
||||
major.Next = commit.Parents[0];
|
||||
if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major);
|
||||
} else {
|
||||
major.Next = "ENDED";
|
||||
ended.Add(l);
|
||||
}
|
||||
|
||||
major.Add(offsetX, offsetY, HALF_HEIGHT);
|
||||
} else {
|
||||
ended.Add(l);
|
||||
}
|
||||
|
||||
isMerged = isMerged || l.IsMerged;
|
||||
} else {
|
||||
if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l);
|
||||
offsetX += UNIT_WIDTH;
|
||||
l.Add(offsetX, offsetY, HALF_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new curve for branch head
|
||||
if (major == null && commit.Parents.Count > 0) {
|
||||
offsetX += UNIT_WIDTH;
|
||||
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
|
||||
unsolved.Add(major);
|
||||
temp.Paths.Add(major.Path);
|
||||
colorIdx = (colorIdx + 1) % colorCount;
|
||||
}
|
||||
|
||||
// Calculate link position of this commit.
|
||||
Point position = new Point(offsetX, offsetY);
|
||||
if (major != null) {
|
||||
major.IsMerged = isMerged;
|
||||
position = new Point(major.LastX, offsetY);
|
||||
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color });
|
||||
} else {
|
||||
temp.Dots.Add(new Dot() { Center = position, Color = 0 });
|
||||
}
|
||||
|
||||
// Deal with parents
|
||||
for (int j = 1; j < commit.Parents.Count; j++) {
|
||||
var parent = commit.Parents[j];
|
||||
if (mapUnsolved.ContainsKey(parent)) {
|
||||
var l = mapUnsolved[parent];
|
||||
var link = new Link();
|
||||
|
||||
link.Start = position;
|
||||
link.End = new Point(l.LastX, offsetY + HALF_HEIGHT);
|
||||
link.Control = new Point(link.End.X, link.Start.Y);
|
||||
link.Color = l.Path.Color;
|
||||
temp.Links.Add(link);
|
||||
} else {
|
||||
offsetX += UNIT_WIDTH;
|
||||
|
||||
// Create new curve for parent commit that not includes before
|
||||
var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT));
|
||||
unsolved.Add(l);
|
||||
temp.Paths.Add(l.Path);
|
||||
colorIdx = (colorIdx + 1) % colorCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ended curves from unsolved
|
||||
foreach (var l in ended) {
|
||||
l.Add(position.X, position.Y, HALF_HEIGHT, true);
|
||||
unsolved.Remove(l);
|
||||
}
|
||||
|
||||
// Margins & merge state (used by datagrid).
|
||||
commit.IsMerged = isMerged;
|
||||
commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH), 0, 0, 0);
|
||||
|
||||
// Clean up
|
||||
ended.Clear();
|
||||
mapUnsolved.Clear();
|
||||
}
|
||||
|
||||
// Deal with curves haven't ended yet.
|
||||
for (int i = 0; i < unsolved.Count; i++) {
|
||||
var path = unsolved[i];
|
||||
var endY = (commits.Count - 0.5) * UNIT_HEIGHT;
|
||||
|
||||
if (path.Path.Points.Count == 1 && path.Path.Points[0].Y == endY) continue;
|
||||
path.Add((i + 0.5) * UNIT_WIDTH, endY + HALF_HEIGHT, HALF_HEIGHT, true);
|
||||
}
|
||||
unsolved.Clear();
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue