refactor<*>: rewrite all with AvaloniaUI

This commit is contained in:
leo 2024-02-06 15:08:37 +08:00
parent 0136904612
commit 2a62596999
521 changed files with 19780 additions and 23244 deletions

1
.gitignore vendored
View file

@ -3,5 +3,4 @@
.vscode
bin
obj
publish
*.user

View file

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

View file

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

View file

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

View file

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

View 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
View file

@ -0,0 +1 @@
dotnet publish -c Release -r win-x64 -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained

View file

@ -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;
public Add(string repo, List<Models.Change> changes = null) {
WorkingDirectory = repo;
Context = repo;
if (changes == null || changes.Count == 0) {
Args = "add .";
}
public Add(string repo, List<string> paths) {
StringBuilder builder = new StringBuilder();
} 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();
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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+%");
}
}

View file

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

View file

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

View file

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

View 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>();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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");
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>();
}
}

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View 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>();
}
}

View file

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

View 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();
}
}
}

View file

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

View 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>();
}
}

View file

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

View 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
View 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>();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);
}
}

View 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);
}
}

View 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}");
}
}

View 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);
}
}

View 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;
});
}
}

View 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();
}
}

View 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;
}
});
}
}

View 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);
}
}

View 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);
});
}
}

View 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);
}
}

View 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;
}
});
}
}

View file

@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura />
</Weavers>

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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