mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-22 12:45:00 +00:00
Project Location
This commit is contained in:
parent
014e37e505
commit
a1a14f8858
305 changed files with 9783 additions and 9783 deletions
21
src/SourceGit/App.axaml
Normal file
21
src/SourceGit/App.axaml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<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/Icons.axaml"/>
|
||||
<ResourceInclude Source="/Resources/Themes.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</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>
|
199
src/SourceGit/App.axaml.cs
Normal file
199
src/SourceGit/App.axaml.cs
Normal file
|
@ -0,0 +1,199 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Fonts;
|
||||
using Avalonia.Styling;
|
||||
|
||||
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.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"SourceGit",
|
||||
$"crash_{time}.log");
|
||||
File.WriteAllText(file, builder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
{
|
||||
var builder = AppBuilder.Configure<App>();
|
||||
builder.UsePlatformDetect();
|
||||
builder.LogToTrace();
|
||||
builder.ConfigureFonts(manager =>
|
||||
{
|
||||
var monospace = new EmbeddedFontCollection(
|
||||
new Uri("fonts:SourceGit", UriKind.Absolute),
|
||||
new Uri("avares://SourceGit/Resources/Fonts", UriKind.Absolute));
|
||||
manager.AddFontCollection(monospace);
|
||||
});
|
||||
|
||||
Native.OS.SetupApp(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static void RaiseException(string context, string message)
|
||||
{
|
||||
if (Current is App app && app._notificationReceiver != null)
|
||||
{
|
||||
var notice = new Models.Notification() { IsError = true, Message = message };
|
||||
app._notificationReceiver.OnReceiveNotification(context, notice);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendNotification(string context, string message)
|
||||
{
|
||||
if (Current is App app && app._notificationReceiver != null)
|
||||
{
|
||||
var notice = new Models.Notification() { IsError = false, Message = message };
|
||||
app._notificationReceiver.OnReceiveNotification(context, notice);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetLocale(string localeKey)
|
||||
{
|
||||
var app = Current as App;
|
||||
var rd = new ResourceDictionary();
|
||||
|
||||
var culture = CultureInfo.GetCultureInfo(localeKey.Replace("_", "-"));
|
||||
SourceGit.Resources.Locales.Culture = culture;
|
||||
|
||||
var sets = SourceGit.Resources.Locales.ResourceManager.GetResourceSet(culture, true, true);
|
||||
foreach (var obj in sets)
|
||||
{
|
||||
if (obj is DictionaryEntry entry)
|
||||
{
|
||||
rd.Add(entry.Key, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (app._activeLocale != null)
|
||||
{
|
||||
app.Resources.MergedDictionaries.Remove(app._activeLocale);
|
||||
}
|
||||
|
||||
app.Resources.MergedDictionaries.Add(rd);
|
||||
app._activeLocale = rd;
|
||||
}
|
||||
|
||||
public static void SetTheme(string theme)
|
||||
{
|
||||
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Current.RequestedThemeVariant = ThemeVariant.Light;
|
||||
}
|
||||
else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Current.RequestedThemeVariant = ThemeVariant.Dark;
|
||||
}
|
||||
else
|
||||
{
|
||||
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 Avalonia.Controls.Shapes.Path CreateMenuIcon(string key)
|
||||
{
|
||||
var icon = new Avalonia.Controls.Shapes.Path();
|
||||
icon.Width = 12;
|
||||
icon.Height = 12;
|
||||
icon.Stretch = Stretch.Uniform;
|
||||
icon.Data = Current.FindResource(key) as StreamGeometry;
|
||||
return icon;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
BIN
src/SourceGit/App.ico
Normal file
BIN
src/SourceGit/App.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
18
src/SourceGit/App.manifest
Normal file
18
src/SourceGit/App.manifest
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- 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>
|
31
src/SourceGit/Commands/Add.cs
Normal file
31
src/SourceGit/Commands/Add.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Add : Command
|
||||
{
|
||||
public Add(string repo, List<Models.Change> changes = null)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
if (changes == null || changes.Count == 0)
|
||||
{
|
||||
Args = "add .";
|
||||
}
|
||||
else
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("add --");
|
||||
foreach (var c in changes)
|
||||
{
|
||||
builder.Append(" \"");
|
||||
builder.Append(c.Path);
|
||||
builder.Append("\"");
|
||||
}
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
src/SourceGit/Commands/Apply.cs
Normal file
16
src/SourceGit/Commands/Apply.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Apply : Command
|
||||
{
|
||||
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "apply ";
|
||||
if (ignoreWhitespace) Args += "--ignore-whitespace ";
|
||||
else Args += $"--whitespace={whitespaceMode} ";
|
||||
if (!string.IsNullOrEmpty(extra)) Args += $"{extra} ";
|
||||
Args += $"\"{file}\"";
|
||||
}
|
||||
}
|
||||
}
|
23
src/SourceGit/Commands/Archive.cs
Normal file
23
src/SourceGit/Commands/Archive.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Archive : Command
|
||||
{
|
||||
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;
|
||||
_outputHandler = outputHandler;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler;
|
||||
}
|
||||
}
|
75
src/SourceGit/Commands/AssumeUnchanged.cs
Normal file
75
src/SourceGit/Commands/AssumeUnchanged.cs
Normal file
|
@ -0,0 +1,75 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class AssumeUnchanged
|
||||
{
|
||||
partial class ViewCommand : Command
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^(\w)\s+(.+)$")]
|
||||
private static partial Regex REG();
|
||||
|
||||
public ViewCommand(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Args = "ls-files -v";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public List<string> Result()
|
||||
{
|
||||
Exec();
|
||||
return _outs;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly 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";
|
||||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"update-index {mode} -- \"{file}\"";
|
||||
}
|
||||
}
|
||||
|
||||
public AssumeUnchanged(string repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public List<string> View()
|
||||
{
|
||||
return new ViewCommand(_repo).Result();
|
||||
}
|
||||
|
||||
public void Add(string file)
|
||||
{
|
||||
new ModCommand(_repo, file, true).Exec();
|
||||
}
|
||||
|
||||
public void Remove(string file)
|
||||
{
|
||||
new ModCommand(_repo, file, false).Exec();
|
||||
}
|
||||
|
||||
private readonly string _repo;
|
||||
}
|
||||
}
|
93
src/SourceGit/Commands/Blame.cs
Normal file
93
src/SourceGit/Commands/Blame.cs
Normal file
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class Blame : Command
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
public Blame(string repo, string file, string revision)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"blame -t {revision} -- \"{file}\"";
|
||||
RaiseError = false;
|
||||
|
||||
_result.File = file;
|
||||
}
|
||||
|
||||
public Models.BlameData Result()
|
||||
{
|
||||
var succ = Exec();
|
||||
if (!succ)
|
||||
{
|
||||
return new Models.BlameData();
|
||||
}
|
||||
|
||||
if (_needUnifyCommitSHA)
|
||||
{
|
||||
foreach (var line in _result.LineInfos)
|
||||
{
|
||||
if (line.CommitSHA.Length > _minSHALen)
|
||||
{
|
||||
line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_result.Content = _content.ToString();
|
||||
return _result;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
if (_result.IsBinary) return;
|
||||
if (string.IsNullOrEmpty(line)) return;
|
||||
|
||||
if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
_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;
|
||||
var author = match.Groups[2].Value;
|
||||
var timestamp = int.Parse(match.Groups[3].Value);
|
||||
var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd");
|
||||
|
||||
var info = new Models.BlameLineInfo()
|
||||
{
|
||||
IsFirstInGroup = commit != _lastSHA,
|
||||
CommitSHA = commit,
|
||||
Author = author,
|
||||
Time = when,
|
||||
};
|
||||
|
||||
_result.LineInfos.Add(info);
|
||||
_lastSHA = commit;
|
||||
|
||||
if (line[0] == '^')
|
||||
{
|
||||
_needUnifyCommitSHA = true;
|
||||
_minSHALen = Math.Min(_minSHALen, commit.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Models.BlameData _result = new Models.BlameData();
|
||||
private readonly StringBuilder _content = new StringBuilder();
|
||||
private string _lastSHA = string.Empty;
|
||||
private bool _needUnifyCommitSHA = false;
|
||||
private int _minSHALen = 64;
|
||||
}
|
||||
}
|
48
src/SourceGit/Commands/Branch.cs
Normal file
48
src/SourceGit/Commands/Branch.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
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 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 static bool SetUpstream(string repo, string name, string upstream)
|
||||
{
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = repo;
|
||||
cmd.Context = repo;
|
||||
if (string.IsNullOrEmpty(upstream))
|
||||
{
|
||||
cmd.Args = $"branch {name} --unset-upstream";
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd.Args = $"branch {name} -u {upstream}";
|
||||
}
|
||||
return cmd.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();
|
||||
}
|
||||
}
|
||||
}
|
72
src/SourceGit/Commands/Checkout.cs
Normal file
72
src/SourceGit/Commands/Checkout.cs
Normal file
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Checkout : Command
|
||||
{
|
||||
public Checkout(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Branch(string branch, Action<string> onProgress)
|
||||
{
|
||||
Args = $"checkout --progress {branch}";
|
||||
TraitErrorAsOutput = true;
|
||||
_outputHandler = onProgress;
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Branch(string branch, string basedOn, Action<string> onProgress)
|
||||
{
|
||||
Args = $"checkout --progress -b {branch} {basedOn}";
|
||||
TraitErrorAsOutput = true;
|
||||
_outputHandler = onProgress;
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool File(string file, bool useTheirs)
|
||||
{
|
||||
if (useTheirs)
|
||||
{
|
||||
Args = $"checkout --theirs -- \"{file}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = $"checkout --ours -- \"{file}\"";
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool FileWithRevision(string file, string revision)
|
||||
{
|
||||
Args = $"checkout {revision} -- \"{file}\"";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Files(List<string> files)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("checkout -f -q --");
|
||||
foreach (var f in files)
|
||||
{
|
||||
builder.Append(" \"");
|
||||
builder.Append(f);
|
||||
builder.Append("\"");
|
||||
}
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
13
src/SourceGit/Commands/CherryPick.cs
Normal file
13
src/SourceGit/Commands/CherryPick.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class CherryPick : Command
|
||||
{
|
||||
public CherryPick(string repo, string commit, bool noCommit)
|
||||
{
|
||||
var mode = noCommit ? "-n" : "--ff";
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"cherry-pick {mode} {commit}";
|
||||
}
|
||||
}
|
||||
}
|
31
src/SourceGit/Commands/Clean.cs
Normal file
31
src/SourceGit/Commands/Clean.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Clean : Command
|
||||
{
|
||||
public Clean(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "clean -qfd";
|
||||
}
|
||||
|
||||
public Clean(string repo, List<string> files)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("clean -qfd --");
|
||||
foreach (var f in files)
|
||||
{
|
||||
builder.Append(" \"");
|
||||
builder.Append(f);
|
||||
builder.Append("\"");
|
||||
}
|
||||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
38
src/SourceGit/Commands/Clone.cs
Normal file
38
src/SourceGit/Commands/Clone.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Clone : Command
|
||||
{
|
||||
private readonly Action<string> _notifyProgress;
|
||||
|
||||
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler)
|
||||
{
|
||||
Context = ctx;
|
||||
WorkingDirectory = path;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
if (string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
|
||||
Args += "clone --progress --verbose --recurse-submodules ";
|
||||
|
||||
if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} ";
|
||||
Args += $"{url} ";
|
||||
if (!string.IsNullOrEmpty(localName)) Args += localName;
|
||||
|
||||
_notifyProgress = ouputHandler;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_notifyProgress?.Invoke(line);
|
||||
}
|
||||
}
|
||||
}
|
180
src/SourceGit/Commands/Command.cs
Normal file
180
src/SourceGit/Commands/Command.cs
Normal file
|
@ -0,0 +1,180 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class Command
|
||||
{
|
||||
public class CancelToken
|
||||
{
|
||||
public bool Requested { get; set; } = false;
|
||||
}
|
||||
|
||||
public class ReadToEndResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public string StdOut { get; set; }
|
||||
public string StdErr { get; set; }
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
public bool Exec()
|
||||
{
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = Native.OS.GitInstallPath;
|
||||
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.RedirectStandardOutput = true;
|
||||
start.RedirectStandardError = true;
|
||||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
// Force using en_US.UTF-8 locale to avoid GCM crash
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
start.Environment.Add("LANG", "en_US.UTF-8");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
|
||||
|
||||
var errs = new List<string>();
|
||||
var proc = new Process() { StartInfo = start };
|
||||
var isCancelled = false;
|
||||
|
||||
proc.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (Cancel != null && Cancel.Requested)
|
||||
{
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
if (!proc.HasExited) proc.Kill(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Data != null) OnReadline(e.Data);
|
||||
};
|
||||
|
||||
proc.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (Cancel != null && Cancel.Requested)
|
||||
{
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
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 (_progressRegex().IsMatch(e.Data)) return;
|
||||
errs.Add(e.Data);
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
proc.Start();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (RaiseError)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(Context, e.Message);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
proc.WaitForExit();
|
||||
|
||||
int exitCode = proc.ExitCode;
|
||||
proc.Close();
|
||||
|
||||
if (!isCancelled && exitCode != 0 && errs.Count > 0)
|
||||
{
|
||||
if (RaiseError)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(Context, string.Join("\n", errs));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ReadToEndResult ReadToEnd()
|
||||
{
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = Native.OS.GitInstallPath;
|
||||
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.RedirectStandardOutput = true;
|
||||
start.RedirectStandardError = true;
|
||||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
|
||||
|
||||
var proc = new Process() { StartInfo = start };
|
||||
try
|
||||
{
|
||||
proc.Start();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ReadToEndResult()
|
||||
{
|
||||
IsSuccess = false,
|
||||
StdOut = string.Empty,
|
||||
StdErr = e.Message,
|
||||
};
|
||||
}
|
||||
|
||||
var rs = new ReadToEndResult()
|
||||
{
|
||||
StdOut = proc.StandardOutput.ReadToEnd(),
|
||||
StdErr = proc.StandardError.ReadToEnd(),
|
||||
};
|
||||
|
||||
proc.WaitForExit();
|
||||
rs.IsSuccess = proc.ExitCode == 0;
|
||||
proc.Close();
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
protected virtual void OnReadline(string line) { }
|
||||
|
||||
[GeneratedRegex(@"\d+%")]
|
||||
private static partial Regex _progressRegex();
|
||||
}
|
||||
}
|
19
src/SourceGit/Commands/Commit.cs
Normal file
19
src/SourceGit/Commands/Commit.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Commit : Command
|
||||
{
|
||||
public Commit(string repo, string message, bool amend, bool allowEmpty = false)
|
||||
{
|
||||
var file = Path.GetTempFileName();
|
||||
File.WriteAllText(file, message);
|
||||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"commit --file=\"{file}\"";
|
||||
if (amend) Args += " --amend --no-edit";
|
||||
if (allowEmpty) Args += " --allow-empty";
|
||||
}
|
||||
}
|
||||
}
|
45
src/SourceGit/Commands/CompareRevisions.cs
Normal file
45
src/SourceGit/Commands/CompareRevisions.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class CompareRevisions : Command
|
||||
{
|
||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
public CompareRevisions(string repo, string start, string end)
|
||||
{
|
||||
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 readonly List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
80
src/SourceGit/Commands/Config.cs
Normal file
80
src/SourceGit/Commands/Config.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Config : Command
|
||||
{
|
||||
public Config(string repository)
|
||||
{
|
||||
WorkingDirectory = repository;
|
||||
Context = repository;
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> ListAll()
|
||||
{
|
||||
Args = "config -l";
|
||||
|
||||
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('=', StringComparison.Ordinal);
|
||||
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().StdOut.Trim();
|
||||
}
|
||||
|
||||
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.IsNullOrWhiteSpace(WorkingDirectory))
|
||||
{
|
||||
Args = $"config --global {key} \"{value}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = $"config {key} \"{value}\"";
|
||||
}
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
207
src/SourceGit/Commands/Diff.cs
Normal file
207
src/SourceGit/Commands/Diff.cs
Normal file
|
@ -0,0 +1,207 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class Diff : Command
|
||||
{
|
||||
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
|
||||
private static partial Regex REG_INDICATOR();
|
||||
private static readonly string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/";
|
||||
private static readonly string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/";
|
||||
private static readonly string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/";
|
||||
|
||||
public Diff(string repo, Models.DiffOption opt)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff --ignore-cr-at-eol --unified=4 {opt}";
|
||||
}
|
||||
|
||||
public Models.DiffResult Result()
|
||||
{
|
||||
Exec();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return _result;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
if (_result.IsBinary) return;
|
||||
|
||||
if (_result.IsLFS)
|
||||
{
|
||||
var ch = line[0];
|
||||
if (ch == '-')
|
||||
{
|
||||
if (line.StartsWith("-oid sha256:", StringComparison.Ordinal))
|
||||
{
|
||||
_result.LFSDiff.Old.Oid = line.Substring(12);
|
||||
}
|
||||
else if (line.StartsWith("-size ", StringComparison.Ordinal))
|
||||
{
|
||||
_result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
|
||||
}
|
||||
}
|
||||
else if (ch == '+')
|
||||
{
|
||||
if (line.StartsWith("+oid sha256:", StringComparison.Ordinal))
|
||||
{
|
||||
_result.LFSDiff.New.Oid = line.Substring(12);
|
||||
}
|
||||
else if (line.StartsWith("+size ", StringComparison.Ordinal))
|
||||
{
|
||||
_result.LFSDiff.New.Size = long.Parse(line.Substring(6));
|
||||
}
|
||||
}
|
||||
else if (line.StartsWith(" size ", StringComparison.Ordinal))
|
||||
{
|
||||
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (line.Length == 0)
|
||||
{
|
||||
ProcessInlineHighlights();
|
||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
|
||||
_oldLine++;
|
||||
_newLine++;
|
||||
return;
|
||||
}
|
||||
|
||||
var ch = line[0];
|
||||
if (ch == '-')
|
||||
{
|
||||
if (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal))
|
||||
{
|
||||
_result.IsLFS = true;
|
||||
_result.LFSDiff = new Models.LFSDiff();
|
||||
return;
|
||||
}
|
||||
|
||||
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
|
||||
_oldLine++;
|
||||
}
|
||||
else if (ch == '+')
|
||||
{
|
||||
if (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal))
|
||||
{
|
||||
_result.IsLFS = true;
|
||||
_result.LFSDiff = new Models.LFSDiff();
|
||||
return;
|
||||
}
|
||||
|
||||
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
|
||||
_newLine++;
|
||||
}
|
||||
else if (ch != '\\')
|
||||
{
|
||||
ProcessInlineHighlights();
|
||||
var match = REG_INDICATOR().Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
_oldLine = int.Parse(match.Groups[1].Value);
|
||||
_newLine = int.Parse(match.Groups[2].Value);
|
||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal))
|
||||
{
|
||||
_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 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.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.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount));
|
||||
}
|
||||
|
||||
if (chunk.AddedCount > 0)
|
||||
{
|
||||
right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_result.TextDiff.Lines.AddRange(_deleted);
|
||||
_deleted.Clear();
|
||||
}
|
||||
|
||||
if (_added.Count > 0)
|
||||
{
|
||||
_result.TextDiff.Lines.AddRange(_added);
|
||||
_added.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() };
|
||||
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
|
||||
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
|
||||
private int _oldLine = 0;
|
||||
private int _newLine = 0;
|
||||
}
|
||||
}
|
55
src/SourceGit/Commands/Discard.cs
Normal file
55
src/SourceGit/Commands/Discard.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public static class Discard
|
||||
{
|
||||
public static void All(string repo)
|
||||
{
|
||||
new Reset(repo, "HEAD", "--hard").Exec();
|
||||
new Clean(repo).Exec();
|
||||
}
|
||||
|
||||
public static void ChangesInWorkTree(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.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
|
||||
{
|
||||
needClean.Add(c.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
needCheckout.Add(c.Path);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < needClean.Count; i += 10)
|
||||
{
|
||||
var count = Math.Min(10, needClean.Count - i);
|
||||
new Clean(repo, needClean.GetRange(i, count)).Exec();
|
||||
}
|
||||
|
||||
for (int i = 0; i < needCheckout.Count; i += 10)
|
||||
{
|
||||
var count = Math.Min(10, needCheckout.Count - i);
|
||||
new Checkout(repo).Files(needCheckout.GetRange(i, count));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ChangesInStaged(string repo, List<Models.Change> changes)
|
||||
{
|
||||
for (int i = 0; i < changes.Count; i += 10)
|
||||
{
|
||||
var count = Math.Min(10, changes.Count - i);
|
||||
var files = new List<string>();
|
||||
for (int j = 0; j < count; j++) files.Add(changes[i + j].Path);
|
||||
new Restore(repo, files, "--staged --worktree").Exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
src/SourceGit/Commands/Fetch.cs
Normal file
156
src/SourceGit/Commands/Fetch.cs
Normal file
|
@ -0,0 +1,156 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Fetch : Command
|
||||
{
|
||||
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
|
||||
Args += "fetch --progress --verbose ";
|
||||
if (prune) Args += "--prune ";
|
||||
Args += remote;
|
||||
|
||||
AutoFetch.MarkFetched(repo);
|
||||
}
|
||||
|
||||
public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
|
||||
Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler;
|
||||
}
|
||||
|
||||
public class AutoFetch
|
||||
{
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
class Job
|
||||
{
|
||||
public Fetch Cmd = null;
|
||||
public DateTime NextRunTimepoint = DateTime.MinValue;
|
||||
}
|
||||
|
||||
static AutoFetch()
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
Thread.Sleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var uptodate = new List<Job>();
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var job in _jobs)
|
||||
{
|
||||
if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0)
|
||||
{
|
||||
uptodate.Add(job.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var job in uptodate)
|
||||
{
|
||||
job.Cmd.Exec();
|
||||
job.NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval);
|
||||
}
|
||||
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddRepository(string repo)
|
||||
{
|
||||
var job = new Job
|
||||
{
|
||||
Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false },
|
||||
NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval),
|
||||
};
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_jobs.ContainsKey(repo))
|
||||
{
|
||||
_jobs[repo] = job;
|
||||
}
|
||||
else
|
||||
{
|
||||
_jobs.Add(repo, job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveRepository(string repo)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_jobs.Remove(repo);
|
||||
}
|
||||
}
|
||||
|
||||
public static void MarkFetched(string repo)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_jobs.ContainsKey(repo))
|
||||
{
|
||||
_jobs[repo].NextRunTimepoint = DateTime.Now.AddSeconds(_fetchInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, Job> _jobs = new Dictionary<string, Job>();
|
||||
private static readonly object _lock = new object();
|
||||
private static readonly double _fetchInterval = 10 * 60;
|
||||
}
|
||||
}
|
12
src/SourceGit/Commands/FormatPatch.cs
Normal file
12
src/SourceGit/Commands/FormatPatch.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class FormatPatch : Command
|
||||
{
|
||||
public FormatPatch(string repo, string commit, string saveTo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"format-patch {commit} -1 -o \"{saveTo}\"";
|
||||
}
|
||||
}
|
||||
}
|
23
src/SourceGit/Commands/GC.cs
Normal file
23
src/SourceGit/Commands/GC.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class GC : Command
|
||||
{
|
||||
public GC(string repo, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
Args = "gc";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler;
|
||||
}
|
||||
}
|
88
src/SourceGit/Commands/GitFlow.cs
Normal file
88
src/SourceGit/Commands/GitFlow.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class GitFlow : Command
|
||||
{
|
||||
public GitFlow(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
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) Branch.Create(WorkingDirectory, master, current.Head);
|
||||
|
||||
var devBranch = branches.Find(x => x.Name == develop);
|
||||
if (devBranch == null && current != null) Branch.Create(WorkingDirectory, develop, current.Head);
|
||||
|
||||
var cmd = new Config(WorkingDirectory);
|
||||
cmd.Set("gitflow.branch.master", master);
|
||||
cmd.Set("gitflow.branch.develop", develop);
|
||||
cmd.Set("gitflow.prefix.feature", feature);
|
||||
cmd.Set("gitflow.prefix.bugfix", "bugfix/");
|
||||
cmd.Set("gitflow.prefix.release", release);
|
||||
cmd.Set("gitflow.prefix.hotfix", hotfix);
|
||||
cmd.Set("gitflow.prefix.support", "support/");
|
||||
cmd.Set("gitflow.prefix.versiontag", version, true);
|
||||
|
||||
Args = "flow init -d";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Start(Models.GitFlowBranchType type, string name)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Models.GitFlowBranchType.Feature:
|
||||
Args = $"flow feature start {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Release:
|
||||
Args = $"flow release start {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Hotfix:
|
||||
Args = $"flow hotfix start {name}";
|
||||
break;
|
||||
default:
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(Context, "Bad branch type!!!");
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch)
|
||||
{
|
||||
var option = keepBranch ? "-k" : string.Empty;
|
||||
switch (type)
|
||||
{
|
||||
case Models.GitFlowBranchType.Feature:
|
||||
Args = $"flow feature finish {option} {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Release:
|
||||
Args = $"flow release finish {option} {name} -m \"RELEASE_DONE\"";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Hotfix:
|
||||
Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\"";
|
||||
break;
|
||||
default:
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(Context, "Bad branch type!!!");
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
12
src/SourceGit/Commands/Init.cs
Normal file
12
src/SourceGit/Commands/Init.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Init : Command
|
||||
{
|
||||
public Init(string ctx, string dir)
|
||||
{
|
||||
Context = ctx;
|
||||
WorkingDirectory = dir;
|
||||
Args = "init -q";
|
||||
}
|
||||
}
|
||||
}
|
23
src/SourceGit/Commands/IsBinary.cs
Normal file
23
src/SourceGit/Commands/IsBinary.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class IsBinary : Command
|
||||
{
|
||||
[GeneratedRegex(@"^\-\s+\-\s+.*$")]
|
||||
private static partial Regex REG_TEST();
|
||||
|
||||
public IsBinary(string repo, string commit, string path)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public bool Result()
|
||||
{
|
||||
return REG_TEST().IsMatch(ReadToEnd().StdOut);
|
||||
}
|
||||
}
|
||||
}
|
19
src/SourceGit/Commands/IsLFSFiltered.cs
Normal file
19
src/SourceGit/Commands/IsLFSFiltered.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
}
|
48
src/SourceGit/Commands/LFS.cs
Normal file
48
src/SourceGit/Commands/LFS.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class LFS
|
||||
{
|
||||
class PruneCmd : Command
|
||||
{
|
||||
public PruneCmd(string repo, Action<string> onProgress)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "lfs prune";
|
||||
TraitErrorAsOutput = true;
|
||||
_outputHandler = onProgress;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler;
|
||||
}
|
||||
|
||||
public LFS(string repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public bool IsEnabled()
|
||||
{
|
||||
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 void Prune(Action<string> outputHandler)
|
||||
{
|
||||
new PruneCmd(_repo, outputHandler).Exec();
|
||||
}
|
||||
|
||||
private readonly string _repo;
|
||||
}
|
||||
}
|
23
src/SourceGit/Commands/Merge.cs
Normal file
23
src/SourceGit/Commands/Merge.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Merge : Command
|
||||
{
|
||||
public Merge(string repo, string source, string mode, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
Args = $"merge --progress {source} {mode}";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler = null;
|
||||
}
|
||||
}
|
63
src/SourceGit/Commands/MergeTool.cs
Normal file
63
src/SourceGit/Commands/MergeTool.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
using System.IO;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
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))
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(repo, "Invalid external merge tool settings!");
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(tool))
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
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))
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(repo, "Invalid external merge tool settings!");
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(tool))
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
36
src/SourceGit/Commands/Pull.cs
Normal file
36
src/SourceGit/Commands/Pull.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Pull : Command
|
||||
{
|
||||
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler)
|
||||
{
|
||||
_outputHandler = outputHandler;
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
|
||||
Args += "pull --verbose --progress --tags ";
|
||||
if (useRebase) Args += "--rebase ";
|
||||
Args += $"{remote} {branch}";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler;
|
||||
}
|
||||
}
|
85
src/SourceGit/Commands/Push.cs
Normal file
85
src/SourceGit/Commands/Push.cs
Normal file
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Push : Command
|
||||
{
|
||||
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
_outputHandler = onProgress;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
|
||||
Args += "push --progress --verbose ";
|
||||
|
||||
if (withTags) Args += "--tags ";
|
||||
if (track) Args += "-u ";
|
||||
if (force) Args += "--force-with-lease ";
|
||||
|
||||
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)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
|
||||
Args += $"push {remote} --delete {branch}";
|
||||
}
|
||||
|
||||
public Push(string repo, string remote, string tag, bool isDelete)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
|
||||
if (!string.IsNullOrEmpty(sshKey))
|
||||
{
|
||||
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = "-c credential.helper=manager ";
|
||||
}
|
||||
|
||||
Args += "push ";
|
||||
if (isDelete) Args += "--delete ";
|
||||
Args += $"{remote} refs/tags/{tag}";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private readonly Action<string> _outputHandler = null;
|
||||
}
|
||||
}
|
105
src/SourceGit/Commands/QueryBranches.cs
Normal file
105
src/SourceGit/Commands/QueryBranches.cs
Normal file
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryBranches : Command
|
||||
{
|
||||
private static readonly string PREFIX_LOCAL = "refs/heads/";
|
||||
private static readonly string PREFIX_REMOTE = "refs/remotes/";
|
||||
|
||||
[GeneratedRegex(@"^(\d+)\s(\d+)$")]
|
||||
private static partial Regex REG_AHEAD_BEHIND();
|
||||
|
||||
public QueryBranches(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:trackshort)\"";
|
||||
}
|
||||
|
||||
public List<Models.Branch> Result()
|
||||
{
|
||||
Exec();
|
||||
|
||||
foreach (var b in _branches)
|
||||
{
|
||||
if (b.IsLocal && !string.IsNullOrEmpty(b.UpstreamTrackStatus))
|
||||
{
|
||||
if (b.UpstreamTrackStatus == "=")
|
||||
{
|
||||
b.UpstreamTrackStatus = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
b.UpstreamTrackStatus = ParseTrackStatus(b.Name, b.Upstream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _branches;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
var parts = line.Split('$');
|
||||
if (parts.Length != 5) return;
|
||||
|
||||
var branch = new Models.Branch();
|
||||
var refName = parts[0];
|
||||
if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) return;
|
||||
|
||||
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal))
|
||||
{
|
||||
branch.Name = refName.Substring(PREFIX_LOCAL.Length);
|
||||
branch.IsLocal = true;
|
||||
}
|
||||
else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal))
|
||||
{
|
||||
var name = refName.Substring(PREFIX_REMOTE.Length);
|
||||
var shortNameIdx = name.IndexOf('/', StringComparison.Ordinal);
|
||||
if (shortNameIdx < 0) return;
|
||||
|
||||
branch.Remote = name.Substring(0, shortNameIdx);
|
||||
branch.Name = name.Substring(branch.Remote.Length + 1);
|
||||
branch.IsLocal = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
branch.Name = refName;
|
||||
branch.IsLocal = true;
|
||||
}
|
||||
|
||||
branch.FullName = refName;
|
||||
branch.Head = parts[1];
|
||||
branch.IsCurrent = parts[2] == "*";
|
||||
branch.Upstream = parts[3];
|
||||
branch.UpstreamTrackStatus = parts[4];
|
||||
_branches.Add(branch);
|
||||
}
|
||||
|
||||
private string ParseTrackStatus(string local, string upstream)
|
||||
{
|
||||
var cmd = new Command();
|
||||
cmd.WorkingDirectory = WorkingDirectory;
|
||||
cmd.Context = Context;
|
||||
cmd.Args = $"rev-list --left-right --count {local}...{upstream}";
|
||||
|
||||
var rs = cmd.ReadToEnd();
|
||||
if (!rs.IsSuccess) return string.Empty;
|
||||
|
||||
var match = REG_AHEAD_BEHIND().Match(rs.StdOut);
|
||||
if (!match.Success) return string.Empty;
|
||||
|
||||
var ahead = int.Parse(match.Groups[1].Value);
|
||||
var behind = int.Parse(match.Groups[2].Value);
|
||||
var track = "";
|
||||
if (ahead > 0) track += $"{ahead}↑";
|
||||
if (behind > 0) track += $" {behind}↓";
|
||||
return track.Trim();
|
||||
}
|
||||
|
||||
private readonly List<Models.Branch> _branches = new List<Models.Branch>();
|
||||
}
|
||||
}
|
45
src/SourceGit/Commands/QueryCommitChanges.cs
Normal file
45
src/SourceGit/Commands/QueryCommitChanges.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryCommitChanges : Command
|
||||
{
|
||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
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 readonly List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
200
src/SourceGit/Commands/QueryCommits.cs
Normal file
200
src/SourceGit/Commands/QueryCommits.cs
Normal file
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class QueryCommits : Command
|
||||
{
|
||||
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
|
||||
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
|
||||
|
||||
private readonly List<Models.Commit> commits = new List<Models.Commit>();
|
||||
private Models.Commit current = null;
|
||||
private bool isSkipingGpgsig = false;
|
||||
private bool isHeadFounded = false;
|
||||
private readonly bool findFirstMerged = true;
|
||||
|
||||
public QueryCommits(string repo, string limits, bool needFindHead = true)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Args = "log --date-order --decorate=full --pretty=raw " + limits;
|
||||
findFirstMerged = needFindHead;
|
||||
}
|
||||
|
||||
public List<Models.Commit> Result()
|
||||
{
|
||||
Exec();
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
current.Message = current.Message.Trim();
|
||||
commits.Add(current);
|
||||
}
|
||||
|
||||
if (findFirstMerged && !isHeadFounded && commits.Count > 0)
|
||||
{
|
||||
MarkFirstMerged();
|
||||
}
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
if (isSkipingGpgsig)
|
||||
{
|
||||
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false;
|
||||
return;
|
||||
}
|
||||
else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal))
|
||||
{
|
||||
isSkipingGpgsig = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("commit ", StringComparison.Ordinal))
|
||||
{
|
||||
if (current != null)
|
||||
{
|
||||
current.Message = current.Message.Trim();
|
||||
commits.Add(current);
|
||||
}
|
||||
|
||||
current = new Models.Commit();
|
||||
line = line.Substring(7);
|
||||
|
||||
var decoratorStart = line.IndexOf('(', StringComparison.Ordinal);
|
||||
if (decoratorStart < 0)
|
||||
{
|
||||
current.SHA = line.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.SHA = line.Substring(0, decoratorStart).Trim();
|
||||
current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1));
|
||||
if (!isHeadFounded) isHeadFounded = current.IsMerged;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (current == null) return;
|
||||
|
||||
if (line.StartsWith("tree ", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (line.StartsWith("parent ", StringComparison.Ordinal))
|
||||
{
|
||||
current.Parents.Add(line.Substring("parent ".Length));
|
||||
}
|
||||
else if (line.StartsWith("author ", StringComparison.Ordinal))
|
||||
{
|
||||
Models.User user = Models.User.Invalid;
|
||||
ulong time = 0;
|
||||
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
|
||||
current.Author = user;
|
||||
current.AuthorTime = time;
|
||||
}
|
||||
else if (line.StartsWith("committer ", StringComparison.Ordinal))
|
||||
{
|
||||
Models.User user = Models.User.Invalid;
|
||||
ulong time = 0;
|
||||
Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time);
|
||||
current.Committer = user;
|
||||
current.CommitterTime = time;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(current.Subject))
|
||||
{
|
||||
current.Subject = line.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Message += (line.Trim() + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParseDecorators(List<Models.Decorator> decorators, string data)
|
||||
{
|
||||
bool isHeadOfCurrent = false;
|
||||
|
||||
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
var d = sub.Trim();
|
||||
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal))
|
||||
{
|
||||
decorators.Add(new Models.Decorator()
|
||||
{
|
||||
Type = Models.DecoratorType.Tag,
|
||||
Name = d.Substring(15).Trim(),
|
||||
});
|
||||
}
|
||||
else if (d.EndsWith("/HEAD", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal))
|
||||
{
|
||||
isHeadOfCurrent = true;
|
||||
decorators.Add(new Models.Decorator()
|
||||
{
|
||||
Type = Models.DecoratorType.CurrentBranchHead,
|
||||
Name = d.Substring(19).Trim(),
|
||||
});
|
||||
}
|
||||
else if (d.StartsWith("refs/heads/", StringComparison.Ordinal))
|
||||
{
|
||||
decorators.Add(new Models.Decorator()
|
||||
{
|
||||
Type = Models.DecoratorType.LocalBranchHead,
|
||||
Name = d.Substring(11).Trim(),
|
||||
});
|
||||
}
|
||||
else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal))
|
||||
{
|
||||
decorators.Add(new Models.Decorator()
|
||||
{
|
||||
Type = Models.DecoratorType.RemoteBranchHead,
|
||||
Name = d.Substring(13).Trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
decorators.Sort((l, r) =>
|
||||
{
|
||||
if (l.Type != r.Type)
|
||||
{
|
||||
return (int)l.Type - (int)r.Type;
|
||||
}
|
||||
else
|
||||
{
|
||||
return l.Name.CompareTo(r.Name);
|
||||
}
|
||||
});
|
||||
|
||||
return isHeadOfCurrent;
|
||||
}
|
||||
|
||||
private void MarkFirstMerged()
|
||||
{
|
||||
Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
|
||||
|
||||
var rs = ReadToEnd();
|
||||
var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (shas.Length == 0) return;
|
||||
|
||||
var set = new HashSet<string>();
|
||||
foreach (var sha in shas) set.Add(sha);
|
||||
|
||||
foreach (var c in commits)
|
||||
{
|
||||
if (set.Contains(c.SHA))
|
||||
{
|
||||
c.IsMerged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
src/SourceGit/Commands/QueryFileContent.cs
Normal file
28
src/SourceGit/Commands/QueryFileContent.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class QueryFileContent : Command
|
||||
{
|
||||
public QueryFileContent(string repo, string revision, string file)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"show {revision}:\"{file}\"";
|
||||
}
|
||||
|
||||
public string Result()
|
||||
{
|
||||
Exec();
|
||||
return _builder.ToString();
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_builder.Append(line);
|
||||
_builder.Append('\n');
|
||||
}
|
||||
|
||||
private readonly StringBuilder _builder = new StringBuilder();
|
||||
}
|
||||
}
|
37
src/SourceGit/Commands/QueryFileSize.cs
Normal file
37
src/SourceGit/Commands/QueryFileSize.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryFileSize : Command
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
public QueryFileSize(string repo, string file, string revision)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"ls-tree {revision} -l -- {file}";
|
||||
}
|
||||
|
||||
public long Result()
|
||||
{
|
||||
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 readonly long _result = 0;
|
||||
}
|
||||
}
|
24
src/SourceGit/Commands/QueryGitDir.cs
Normal file
24
src/SourceGit/Commands/QueryGitDir.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class QueryGitDir : Command
|
||||
{
|
||||
public QueryGitDir(string workDir)
|
||||
{
|
||||
WorkingDirectory = workDir;
|
||||
Args = "rev-parse --git-dir";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public string Result()
|
||||
{
|
||||
var rs = ReadToEnd().StdOut;
|
||||
if (string.IsNullOrEmpty(rs)) return null;
|
||||
|
||||
rs = rs.Trim();
|
||||
if (Path.IsPathRooted(rs)) return rs;
|
||||
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
|
||||
}
|
||||
}
|
||||
}
|
73
src/SourceGit/Commands/QueryLocalChanges.cs
Normal file
73
src/SourceGit/Commands/QueryLocalChanges.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryLocalChanges : Command
|
||||
{
|
||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
private static readonly string[] UNTRACKED = ["no", "all"];
|
||||
|
||||
public QueryLocalChanges(string repo, bool includeUntracked = true)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"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 readonly List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
41
src/SourceGit/Commands/QueryRemotes.cs
Normal file
41
src/SourceGit/Commands/QueryRemotes.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryRemotes : Command
|
||||
{
|
||||
[GeneratedRegex(@"^([\w\.\-]+)\s*(\S+).*$")]
|
||||
private static partial Regex REG_REMOTE();
|
||||
|
||||
public QueryRemotes(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "remote -v";
|
||||
}
|
||||
|
||||
public List<Models.Remote> Result()
|
||||
{
|
||||
Exec();
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
var match = REG_REMOTE().Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var remote = new Models.Remote()
|
||||
{
|
||||
Name = match.Groups[1].Value,
|
||||
URL = match.Groups[2].Value,
|
||||
};
|
||||
|
||||
if (_loaded.Find(x => x.Name == remote.Name) != null) return;
|
||||
_loaded.Add(remote);
|
||||
}
|
||||
|
||||
private readonly List<Models.Remote> _loaded = new List<Models.Remote>();
|
||||
}
|
||||
}
|
19
src/SourceGit/Commands/QueryRepositoryRootPath.cs
Normal file
19
src/SourceGit/Commands/QueryRepositoryRootPath.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
47
src/SourceGit/Commands/QueryRevisionObjects.cs
Normal file
47
src/SourceGit/Commands/QueryRevisionObjects.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryRevisionObjects : Command
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
private readonly List<Models.Object> objects = new List<Models.Object>();
|
||||
|
||||
public QueryRevisionObjects(string repo, string sha)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"ls-tree -r {sha}";
|
||||
}
|
||||
|
||||
public List<Models.Object> Result()
|
||||
{
|
||||
Exec();
|
||||
return objects;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
var match = REG_FORMAT().Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var obj = new Models.Object();
|
||||
obj.SHA = match.Groups[2].Value;
|
||||
obj.Type = Models.ObjectType.Blob;
|
||||
obj.Path = match.Groups[3].Value;
|
||||
|
||||
switch (match.Groups[1].Value)
|
||||
{
|
||||
case "blob": obj.Type = Models.ObjectType.Blob; break;
|
||||
case "tree": obj.Type = Models.ObjectType.Tree; break;
|
||||
case "tag": obj.Type = Models.ObjectType.Tag; break;
|
||||
case "commit": obj.Type = Models.ObjectType.Commit; break;
|
||||
}
|
||||
|
||||
objects.Add(obj);
|
||||
}
|
||||
}
|
||||
}
|
29
src/SourceGit/Commands/QueryStagedFileBlobGuid.cs
Normal file
29
src/SourceGit/Commands/QueryStagedFileBlobGuid.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryStagedFileBlobGuid : Command
|
||||
{
|
||||
[GeneratedRegex(@"^\d+\s+([0-9a-f]+)\s+.*$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
public QueryStagedFileBlobGuid(string repo, string file)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"ls-files -s -- \"{file}\"";
|
||||
}
|
||||
|
||||
public string Result()
|
||||
{
|
||||
var rs = ReadToEnd();
|
||||
var match = REG_FORMAT().Match(rs.StdOut.Trim());
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
45
src/SourceGit/Commands/QueryStashChanges.cs
Normal file
45
src/SourceGit/Commands/QueryStashChanges.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryStashChanges : Command
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
|
||||
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 readonly List<Models.Change> _changes = new List<Models.Change>();
|
||||
}
|
||||
}
|
60
src/SourceGit/Commands/QueryStashes.cs
Normal file
60
src/SourceGit/Commands/QueryStashes.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryStashes : Command
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^Reflog: refs/(stash@\{\d+\}).*$")]
|
||||
private static partial Regex REG_STASH();
|
||||
|
||||
public QueryStashes(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "stash list --pretty=raw";
|
||||
}
|
||||
|
||||
public List<Models.Stash> Result()
|
||||
{
|
||||
Exec();
|
||||
if (_current != null) _stashes.Add(_current);
|
||||
return _stashes;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
if (line.StartsWith("commit ", StringComparison.Ordinal))
|
||||
{
|
||||
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 (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal))
|
||||
{
|
||||
var match = REG_STASH().Match(line);
|
||||
if (match.Success) _current.Name = match.Groups[1].Value;
|
||||
}
|
||||
else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal))
|
||||
{
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
|
||||
private Models.Stash _current = null;
|
||||
}
|
||||
}
|
44
src/SourceGit/Commands/QuerySubmodules.cs
Normal file
44
src/SourceGit/Commands/QuerySubmodules.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QuerySubmodules : Command
|
||||
{
|
||||
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
|
||||
private static partial Regex REG_FORMAT1();
|
||||
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
|
||||
private static partial Regex REG_FORMAT2();
|
||||
|
||||
public QuerySubmodules(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "submodule status";
|
||||
}
|
||||
|
||||
public List<string> Result()
|
||||
{
|
||||
Exec();
|
||||
return _submodules;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
var match = REG_FORMAT1().Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
_submodules.Add(match.Groups[1].Value);
|
||||
return;
|
||||
}
|
||||
|
||||
match = REG_FORMAT2().Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
_submodules.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<string> _submodules = new List<string>();
|
||||
}
|
||||
}
|
44
src/SourceGit/Commands/QueryTags.cs
Normal file
44
src/SourceGit/Commands/QueryTags.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
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 readonly List<Models.Tag> _loaded = new List<Models.Tag>();
|
||||
}
|
||||
}
|
14
src/SourceGit/Commands/Rebase.cs
Normal file
14
src/SourceGit/Commands/Rebase.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Rebase : Command
|
||||
{
|
||||
public Rebase(string repo, string basedOn, bool autoStash)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "rebase ";
|
||||
if (autoStash) Args += "--autostash ";
|
||||
Args += basedOn;
|
||||
}
|
||||
}
|
||||
}
|
41
src/SourceGit/Commands/Remote.cs
Normal file
41
src/SourceGit/Commands/Remote.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Remote : Command
|
||||
{
|
||||
public Remote(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Add(string name, string url)
|
||||
{
|
||||
Args = $"remote add {name} {url}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string name)
|
||||
{
|
||||
Args = $"remote remove {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Rename(string name, string to)
|
||||
{
|
||||
Args = $"remote rename {name} {to}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Prune(string name)
|
||||
{
|
||||
Args = $"remote prune {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool SetURL(string name, string url)
|
||||
{
|
||||
Args = $"remote set-url {name} {url}";
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
38
src/SourceGit/Commands/Reset.cs
Normal file
38
src/SourceGit/Commands/Reset.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Reset : Command
|
||||
{
|
||||
public Reset(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = "reset";
|
||||
}
|
||||
|
||||
public Reset(string repo, List<Models.Change> changes)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("reset --");
|
||||
foreach (var c in changes)
|
||||
{
|
||||
builder.Append(" \"");
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
21
src/SourceGit/Commands/Restore.cs
Normal file
21
src/SourceGit/Commands/Restore.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Restore : Command
|
||||
{
|
||||
public Restore(string repo, List<string> files, string extra)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("restore ");
|
||||
if (!string.IsNullOrEmpty(extra)) builder.Append(extra).Append(" ");
|
||||
builder.Append("--");
|
||||
foreach (var f in files) builder.Append(' ').Append('"').Append(f).Append('"');
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
13
src/SourceGit/Commands/Revert.cs
Normal file
13
src/SourceGit/Commands/Revert.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Revert : Command
|
||||
{
|
||||
public Revert(string repo, string commit, bool autoCommit)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"revert {commit} --no-edit";
|
||||
if (!autoCommit) Args += " --no-commit";
|
||||
}
|
||||
}
|
||||
}
|
57
src/SourceGit/Commands/SaveChangesAsPatch.cs
Normal file
57
src/SourceGit/Commands/SaveChangesAsPatch.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
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.GitInstallPath;
|
||||
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)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(repo, "Save change to patch failed: " + e.Message);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
src/SourceGit/Commands/SaveRevisionFile.cs
Normal file
80
src/SourceGit/Commands/SaveRevisionFile.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public static class SaveRevisionFile
|
||||
{
|
||||
public static void Run(string repo, string revision, string file, string saveTo)
|
||||
{
|
||||
var isLFSFiltered = new IsLFSFiltered(repo, file).Result();
|
||||
if (isLFSFiltered)
|
||||
{
|
||||
var tmpFile = saveTo + ".tmp";
|
||||
if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile))
|
||||
{
|
||||
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
|
||||
}
|
||||
File.Delete(tmpFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
ExecCmd(repo, $"show {revision}:\"{file}\"", saveTo);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
|
||||
{
|
||||
var starter = new ProcessStartInfo();
|
||||
starter.WorkingDirectory = repo;
|
||||
starter.FileName = Native.OS.GitInstallPath;
|
||||
starter.Arguments = args;
|
||||
starter.UseShellExecute = false;
|
||||
starter.CreateNoWindow = true;
|
||||
starter.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
starter.RedirectStandardInput = true;
|
||||
starter.RedirectStandardOutput = true;
|
||||
starter.RedirectStandardError = true;
|
||||
|
||||
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();
|
||||
|
||||
return rs;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
App.RaiseException(repo, "Save file failed: " + e.Message);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
82
src/SourceGit/Commands/Stash.cs
Normal file
82
src/SourceGit/Commands/Stash.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Stash : Command
|
||||
{
|
||||
public Stash(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
public bool Push(string message)
|
||||
{
|
||||
Args = $"stash push -m \"{message}\"";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
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<Models.Change>();
|
||||
foreach (var c in changes)
|
||||
{
|
||||
writer.WriteLine(c.Path);
|
||||
|
||||
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked)
|
||||
{
|
||||
needAdd.Add(c);
|
||||
if (needAdd.Count > 10)
|
||||
{
|
||||
new Add(WorkingDirectory, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needAdd.Count > 0)
|
||||
{
|
||||
new Add(WorkingDirectory, needAdd).Exec();
|
||||
needAdd.Clear();
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
stream.Flush();
|
||||
writer.Close();
|
||||
stream.Close();
|
||||
|
||||
Args = $"stash push -m \"{message}\" --pathspec-from-file=\"{temp}\"";
|
||||
var succ = Exec();
|
||||
File.Delete(temp);
|
||||
return succ;
|
||||
}
|
||||
|
||||
public bool Apply(string name)
|
||||
{
|
||||
Args = $"stash apply -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Pop(string name)
|
||||
{
|
||||
Args = $"stash pop -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Drop(string name)
|
||||
{
|
||||
Args = $"stash drop -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Clear()
|
||||
{
|
||||
Args = "stash clear";
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
37
src/SourceGit/Commands/Statistics.cs
Normal file
37
src/SourceGit/Commands/Statistics.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Statistics : Command
|
||||
{
|
||||
public Statistics(string repo)
|
||||
{
|
||||
_statistics = new Models.Statistics();
|
||||
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
Args = $"log --date-order --branches --remotes --since=\"{_statistics.Since()}\" --pretty=format:\"%ct$%cn\"";
|
||||
}
|
||||
|
||||
public Models.Statistics Result()
|
||||
{
|
||||
Exec();
|
||||
_statistics.Complete();
|
||||
return _statistics;
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
var dateEndIdx = line.IndexOf('$', StringComparison.Ordinal);
|
||||
if (dateEndIdx == -1) return;
|
||||
|
||||
var dateStr = line.Substring(0, dateEndIdx);
|
||||
var date = 0.0;
|
||||
if (!double.TryParse(dateStr, out date)) return;
|
||||
|
||||
_statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
|
||||
}
|
||||
|
||||
private readonly Models.Statistics _statistics = null;
|
||||
}
|
||||
}
|
53
src/SourceGit/Commands/Submodule.cs
Normal file
53
src/SourceGit/Commands/Submodule.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Submodule : Command
|
||||
{
|
||||
public Submodule(string repo)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
}
|
||||
|
||||
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 -- {relativePath}";
|
||||
return Exec();
|
||||
}
|
||||
else
|
||||
{
|
||||
Args = $"submodule update --init -- {relativePath}";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Update()
|
||||
{
|
||||
Args = $"submodule update --rebase --remote";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string relativePath)
|
||||
{
|
||||
Args = $"submodule deinit -f {relativePath}";
|
||||
if (!Exec()) return false;
|
||||
|
||||
Args = $"rm -rf {relativePath}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
_outputHandler?.Invoke(line);
|
||||
}
|
||||
|
||||
private Action<string> _outputHandler;
|
||||
}
|
||||
}
|
48
src/SourceGit/Commands/Tag.cs
Normal file
48
src/SourceGit/Commands/Tag.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
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);
|
||||
cmd.Args += $"-F \"{tmp}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd.Args += $"-m {name}";
|
||||
}
|
||||
|
||||
return cmd.Exec();
|
||||
}
|
||||
|
||||
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 (remotes != null)
|
||||
{
|
||||
foreach (var r in remotes)
|
||||
{
|
||||
new Push(repo, r.Name, name, true).Exec();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
18
src/SourceGit/Commands/Version.cs
Normal file
18
src/SourceGit/Commands/Version.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace SourceGit.Commands
|
||||
{
|
||||
public class Version : Command
|
||||
{
|
||||
public Version()
|
||||
{
|
||||
Args = "--version";
|
||||
RaiseError = false;
|
||||
}
|
||||
|
||||
public string Query()
|
||||
{
|
||||
var rs = ReadToEnd();
|
||||
if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) return string.Empty;
|
||||
return rs.StdOut.Trim().Substring("git version ".Length);
|
||||
}
|
||||
}
|
||||
}
|
14
src/SourceGit/Converters/BookmarkConverters.cs
Normal file
14
src/SourceGit/Converters/BookmarkConverters.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
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);
|
||||
}
|
||||
}
|
10
src/SourceGit/Converters/BoolConverters.cs
Normal file
10
src/SourceGit/Converters/BoolConverters.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
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);
|
||||
}
|
||||
}
|
10
src/SourceGit/Converters/BranchConverters.cs
Normal file
10
src/SourceGit/Converters/BranchConverters.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
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}");
|
||||
}
|
||||
}
|
32
src/SourceGit/Converters/ChangeViewModeConverters.cs
Normal file
32
src/SourceGit/Converters/ChangeViewModeConverters.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
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);
|
||||
}
|
||||
}
|
39
src/SourceGit/Converters/DecoratorTypeConverters.cs
Normal file
39
src/SourceGit/Converters/DecoratorTypeConverters.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
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;
|
||||
});
|
||||
}
|
||||
}
|
16
src/SourceGit/Converters/IntConverters.cs
Normal file
16
src/SourceGit/Converters/IntConverters.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using Avalonia.Data.Converters;
|
||||
|
||||
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 static FuncValueConverter<int, bool> IsOne =
|
||||
new FuncValueConverter<int, bool>(v => v == 1);
|
||||
}
|
||||
}
|
35
src/SourceGit/Converters/LauncherPageConverters.cs
Normal file
35
src/SourceGit/Converters/LauncherPageConverters.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
15
src/SourceGit/Converters/ListConverters.cs
Normal file
15
src/SourceGit/Converters/ListConverters.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.Collections;
|
||||
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
22
src/SourceGit/Converters/PathConverters.cs
Normal file
22
src/SourceGit/Converters/PathConverters.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System.IO;
|
||||
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
73
src/SourceGit/Converters/StringConverters.cs
Normal file
73
src/SourceGit/Converters/StringConverters.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Styling;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
55
src/SourceGit/Converters/WindowStateConverters.cs
Normal file
55
src/SourceGit/Converters/WindowStateConverters.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Converters
|
||||
{
|
||||
public static class WindowStateConverters
|
||||
{
|
||||
public static FuncValueConverter<WindowState, Thickness> ToContentMargin =
|
||||
new FuncValueConverter<WindowState, Thickness>(state =>
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && state == WindowState.Maximized)
|
||||
{
|
||||
return new Thickness(6);
|
||||
}
|
||||
else if (OperatingSystem.IsLinux() && state != WindowState.Maximized)
|
||||
{
|
||||
return new Thickness(6);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Thickness(0);
|
||||
}
|
||||
});
|
||||
|
||||
public static FuncValueConverter<WindowState, GridLength> ToTitleBarHeight =
|
||||
new FuncValueConverter<WindowState, GridLength>(state =>
|
||||
{
|
||||
if (state == WindowState.Maximized)
|
||||
{
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
16
src/SourceGit/Models/ApplyWhiteSpaceMode.cs
Normal file
16
src/SourceGit/Models/ApplyWhiteSpaceMode.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
157
src/SourceGit/Models/AvatarManager.cs
Normal file
157
src/SourceGit/Models/AvatarManager.cs
Normal file
|
@ -0,0 +1,157 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public interface IAvatarHost
|
||||
{
|
||||
void OnAvatarResourceChanged(string md5);
|
||||
}
|
||||
|
||||
public static class AvatarManager
|
||||
{
|
||||
public static string SelectedServer
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = "https://www.gravatar.com/avatar/";
|
||||
|
||||
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($"{SelectedServer}{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);
|
||||
NotifyResourceChanged(md5);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void Subscribe(IAvatarHost host)
|
||||
{
|
||||
_avatars.Add(host);
|
||||
}
|
||||
|
||||
public static void Unsubscribe(IAvatarHost host)
|
||||
{
|
||||
_avatars.Remove(host);
|
||||
}
|
||||
|
||||
public static Bitmap Request(string md5, bool forceRefetch = false)
|
||||
{
|
||||
if (forceRefetch)
|
||||
{
|
||||
if (_resources.ContainsKey(md5)) _resources.Remove(md5);
|
||||
|
||||
var localFile = Path.Combine(_storePath, md5);
|
||||
if (File.Exists(localFile)) File.Delete(localFile);
|
||||
|
||||
NotifyResourceChanged(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 NotifyResourceChanged(string md5)
|
||||
{
|
||||
foreach (var avatar in _avatars)
|
||||
{
|
||||
avatar.OnAvatarResourceChanged(md5);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object _synclock = new object();
|
||||
private static readonly string _storePath = string.Empty;
|
||||
private static readonly List<IAvatarHost> _avatars = new List<IAvatarHost>();
|
||||
private static readonly Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
|
||||
private static readonly HashSet<string> _requesting = new HashSet<string>();
|
||||
}
|
||||
}
|
20
src/SourceGit/Models/Blame.cs
Normal file
20
src/SourceGit/Models/Blame.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
}
|
25
src/SourceGit/Models/Bookmarks.cs
Normal file
25
src/SourceGit/Models/Bookmarks.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
14
src/SourceGit/Models/Branch.cs
Normal file
14
src/SourceGit/Models/Branch.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public class Branch
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FullName { get; set; }
|
||||
public string Head { get; set; }
|
||||
public bool IsLocal { get; set; }
|
||||
public bool IsCurrent { get; set; }
|
||||
public string Upstream { get; set; }
|
||||
public string UpstreamTrackStatus { get; set; }
|
||||
public string Remote { get; set; }
|
||||
}
|
||||
}
|
201
src/SourceGit/Models/BranchTreeNode.cs
Normal file
201
src/SourceGit/Models/BranchTreeNode.cs
Normal file
|
@ -0,0 +1,201 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Avalonia.Collections;
|
||||
|
||||
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 bool IsFiltered { 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)
|
||||
{
|
||||
var isFiltered = _filters.Contains(branch.FullName);
|
||||
if (branch.IsLocal)
|
||||
{
|
||||
MakeBranchNode(branch, _locals, "local", isFiltered);
|
||||
}
|
||||
else
|
||||
{
|
||||
var remote = _remotes.Find(x => x.Name == branch.Remote);
|
||||
if (remote != null) MakeBranchNode(branch, remote.Children, $"remote/{remote.Name}", isFiltered);
|
||||
}
|
||||
}
|
||||
|
||||
SortNodes(_locals);
|
||||
SortNodes(_remotes);
|
||||
}
|
||||
|
||||
public void SetFilters(AvaloniaList<string> filters)
|
||||
{
|
||||
_filters.AddRange(filters);
|
||||
}
|
||||
|
||||
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, bool isFiltered)
|
||||
{
|
||||
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,
|
||||
IsFiltered = isFiltered,
|
||||
};
|
||||
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 = branch.IsCurrent || _expanded.Contains(path),
|
||||
};
|
||||
roots.Add(lastFolder);
|
||||
_maps.Add(path, lastFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
var folder = new BranchTreeNode()
|
||||
{
|
||||
Name = subs[i],
|
||||
Type = BranchTreeNodeType.Folder,
|
||||
IsExpanded = branch.IsCurrent || _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,
|
||||
IsFiltered = isFiltered,
|
||||
};
|
||||
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 readonly List<BranchTreeNode> _locals = new List<BranchTreeNode>();
|
||||
private readonly List<BranchTreeNode> _remotes = new List<BranchTreeNode>();
|
||||
private readonly HashSet<string> _expanded = new HashSet<string>();
|
||||
private readonly List<string> _filters = new List<string>();
|
||||
private readonly Dictionary<string, BranchTreeNode> _maps = new Dictionary<string, BranchTreeNode>();
|
||||
}
|
||||
}
|
||||
}
|
24
src/SourceGit/Models/CRLFMode.cs
Normal file
24
src/SourceGit/Models/CRLFMode.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
70
src/SourceGit/Models/Change.cs
Normal file
70
src/SourceGit/Models/Change.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public enum ChangeViewMode
|
||||
{
|
||||
List,
|
||||
Grid,
|
||||
Tree,
|
||||
}
|
||||
|
||||
public enum ChangeState
|
||||
{
|
||||
None,
|
||||
Modified,
|
||||
Added,
|
||||
Deleted,
|
||||
Renamed,
|
||||
Copied,
|
||||
Unmerged,
|
||||
Untracked
|
||||
}
|
||||
|
||||
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 == 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(ChangeState index, ChangeState workTree = ChangeState.None)
|
||||
{
|
||||
Index = index;
|
||||
WorkTree = workTree;
|
||||
|
||||
if (index == ChangeState.Renamed || workTree == ChangeState.Renamed)
|
||||
{
|
||||
var idx = Path.IndexOf('\t', StringComparison.Ordinal);
|
||||
if (idx >= 0)
|
||||
{
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
Path = Path.Substring(idx + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
idx = Path.IndexOf(" -> ", StringComparison.Ordinal);
|
||||
if (idx > 0)
|
||||
{
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
Path = Path.Substring(idx + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Path[0] == '"') Path = Path.Substring(1, Path.Length - 2);
|
||||
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
|
||||
}
|
||||
}
|
||||
}
|
50
src/SourceGit/Models/Commit.cs
Normal file
50
src/SourceGit/Models/Commit.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Avalonia;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class Commit
|
||||
{
|
||||
public string SHA { get; set; } = string.Empty;
|
||||
public User Author { get; set; } = User.Invalid;
|
||||
public ulong AuthorTime { get; set; } = 0;
|
||||
public User Committer { get; set; } = User.Invalid;
|
||||
public ulong CommitterTime { get; set; } = 0;
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public List<string> Parents { get; set; } = new List<string>();
|
||||
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
|
||||
public bool HasDecorators => Decorators.Count > 0;
|
||||
public bool IsMerged { get; set; } = false;
|
||||
public Thickness Margin { get; set; } = new Thickness(0);
|
||||
|
||||
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('>', StringComparison.Ordinal);
|
||||
if (userEndIdx < 0) return;
|
||||
|
||||
var timeEndIdx = data.IndexOf(' ', userEndIdx + 2);
|
||||
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();
|
||||
}
|
||||
}
|
244
src/SourceGit/Models/CommitGraph.cs
Normal file
244
src/SourceGit/Models/CommitGraph.cs
Normal file
|
@ -0,0 +1,244 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Avalonia;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
27
src/SourceGit/Models/Decorator.cs
Normal file
27
src/SourceGit/Models/Decorator.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public enum DecoratorType
|
||||
{
|
||||
None,
|
||||
CurrentBranchHead,
|
||||
LocalBranchHead,
|
||||
RemoteBranchHead,
|
||||
Tag,
|
||||
}
|
||||
|
||||
public class Decorator
|
||||
{
|
||||
public DecoratorType Type { get; set; } = DecoratorType.None;
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
public static class DecoratorResources
|
||||
{
|
||||
public static readonly IBrush[] Backgrounds = [
|
||||
new SolidColorBrush(0xFF02C302),
|
||||
new SolidColorBrush(0xFFFFB835),
|
||||
];
|
||||
}
|
||||
}
|
113
src/SourceGit/Models/DiffOption.cs
Normal file
113
src/SourceGit/Models/DiffOption.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class DiffOption
|
||||
{
|
||||
public Change WorkingCopyChange => _workingCopyChange;
|
||||
public bool IsUnstaged => _isUnstaged;
|
||||
public List<string> Revisions => _revisions;
|
||||
public string Path => _path;
|
||||
public string OrgPath => _orgPath;
|
||||
|
||||
/// <summary>
|
||||
/// Only used for working copy changes
|
||||
/// </summary>
|
||||
/// <param name="change"></param>
|
||||
/// <param name="isUnstaged"></param>
|
||||
public DiffOption(Change change, bool isUnstaged)
|
||||
{
|
||||
_workingCopyChange = change;
|
||||
_isUnstaged = isUnstaged;
|
||||
|
||||
if (isUnstaged)
|
||||
{
|
||||
switch (change.WorkTree)
|
||||
{
|
||||
case ChangeState.Added:
|
||||
case ChangeState.Untracked:
|
||||
_extra = "--no-index";
|
||||
_path = change.Path;
|
||||
_orgPath = "/dev/null";
|
||||
break;
|
||||
default:
|
||||
_path = change.Path;
|
||||
_orgPath = change.OriginalPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_extra = "--cached";
|
||||
_path = change.Path;
|
||||
_orgPath = change.OriginalPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only used for commit changes.
|
||||
/// </summary>
|
||||
/// <param name="commit"></param>
|
||||
/// <param name="change"></param>
|
||||
public DiffOption(Commit commit, Change change)
|
||||
{
|
||||
var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
|
||||
_revisions.Add(baseRevision);
|
||||
_revisions.Add(commit.SHA);
|
||||
_path = change.Path;
|
||||
_orgPath = change.OriginalPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diff with filepath. Used by FileHistories
|
||||
/// </summary>
|
||||
/// <param name="commit"></param>
|
||||
/// <param name="file"></param>
|
||||
public DiffOption(Commit commit, string file)
|
||||
{
|
||||
var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
|
||||
_revisions.Add(baseRevision);
|
||||
_revisions.Add(commit.SHA);
|
||||
_path = file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to show differences between two revisions.
|
||||
/// </summary>
|
||||
/// <param name="baseRevision"></param>
|
||||
/// <param name="targetRevision"></param>
|
||||
/// <param name="change"></param>
|
||||
public DiffOption(string baseRevision, string targetRevision, Change change)
|
||||
{
|
||||
_revisions.Add(baseRevision);
|
||||
_revisions.Add(targetRevision);
|
||||
_path = change.Path;
|
||||
_orgPath = change.OriginalPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts to diff command arguments.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
if (!string.IsNullOrEmpty(_extra)) builder.Append($"{_extra} ");
|
||||
foreach (var r in _revisions) builder.Append($"{r} ");
|
||||
|
||||
builder.Append("-- ");
|
||||
if (!string.IsNullOrEmpty(_orgPath)) builder.Append($"\"{_orgPath}\" ");
|
||||
builder.Append($"\"{_path}\"");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private readonly Change _workingCopyChange = null;
|
||||
private readonly bool _isUnstaged = false;
|
||||
private readonly string _orgPath = string.Empty;
|
||||
private readonly string _path = string.Empty;
|
||||
private readonly string _extra = string.Empty;
|
||||
private readonly List<string> _revisions = new List<string>();
|
||||
}
|
||||
}
|
560
src/SourceGit/Models/DiffResult.cs
Normal file
560
src/SourceGit/Models/DiffResult.cs
Normal file
|
@ -0,0 +1,560 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public enum TextDiffLineType
|
||||
{
|
||||
None,
|
||||
Normal,
|
||||
Indicator,
|
||||
Added,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
public class TextInlineRange
|
||||
{
|
||||
public int Start { get; set; }
|
||||
public int Count { get; set; }
|
||||
public TextInlineRange(int p, int n) { Start = p; Count = n; }
|
||||
}
|
||||
|
||||
public class TextDiffLine
|
||||
{
|
||||
public TextDiffLineType Type { get; set; } = TextDiffLineType.None;
|
||||
public string Content { get; set; } = "";
|
||||
public int OldLineNumber { get; set; } = 0;
|
||||
public int NewLineNumber { get; set; } = 0;
|
||||
public List<TextInlineRange> Highlights { get; set; } = new List<TextInlineRange>();
|
||||
|
||||
public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString();
|
||||
public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString();
|
||||
|
||||
public TextDiffLine() { }
|
||||
public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine)
|
||||
{
|
||||
Type = type;
|
||||
Content = content;
|
||||
OldLineNumber = oldLine;
|
||||
NewLineNumber = newLine;
|
||||
}
|
||||
}
|
||||
|
||||
public class TextDiffSelection
|
||||
{
|
||||
public int StartLine { get; set; } = 0;
|
||||
public int EndLine { get; set; } = 0;
|
||||
public bool HasChanges { get; set; } = false;
|
||||
public bool HasLeftChanges { get; set; } = false;
|
||||
public int IgnoredAdds { get; set; } = 0;
|
||||
public int IgnoredDeletes { get; set; } = 0;
|
||||
|
||||
public bool IsInRange(int idx)
|
||||
{
|
||||
return idx >= StartLine - 1 && idx < EndLine;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class TextDiff
|
||||
{
|
||||
public string File { get; set; } = string.Empty;
|
||||
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
|
||||
public int MaxLineNumber = 0;
|
||||
|
||||
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
|
||||
{
|
||||
var isTracked = !string.IsNullOrEmpty(fileBlobGuid);
|
||||
var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
|
||||
if (!revert && !isTracked) builder.Append("new file mode 100644\n");
|
||||
builder.Append("index 00000000...").Append(fileGuid).Append('\n');
|
||||
builder.Append("--- ").Append((revert || isTracked) ? $"a/{change.Path}\n" : "/dev/null\n");
|
||||
builder.Append("+++ b/").Append(change.Path).Append('\n');
|
||||
|
||||
var additions = selection.EndLine - selection.StartLine;
|
||||
if (selection.StartLine != 1) additions++;
|
||||
|
||||
if (revert)
|
||||
{
|
||||
var totalLines = Lines.Count - 1;
|
||||
builder.Append($"@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@");
|
||||
for (int i = 1; i <= totalLines; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type != TextDiffLineType.Added) continue;
|
||||
builder.Append(selection.IsInRange(i) ? "\n+" : "\n ").Append(line.Content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("@@ -0,0 +0,").Append(additions).Append(" @@");
|
||||
for (int i = selection.StartLine - 1; i < selection.EndLine; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type != TextDiffLineType.Added) continue;
|
||||
builder.Append("\n+").Append(line.Content);
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append("\n\\ No newline at end of file\n");
|
||||
System.IO.File.WriteAllText(output, builder.ToString());
|
||||
}
|
||||
|
||||
public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, string output)
|
||||
{
|
||||
var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
|
||||
builder.Append("index 00000000...").Append(fileTreeGuid).Append(" 100644\n");
|
||||
builder.Append("--- a/").Append(orgFile).Append('\n');
|
||||
builder.Append("+++ b/").Append(change.Path);
|
||||
|
||||
// If last line of selection is a change. Find one more line.
|
||||
var tail = null as string;
|
||||
if (selection.EndLine < Lines.Count)
|
||||
{
|
||||
var lastLine = Lines[selection.EndLine - 1];
|
||||
if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
for (int i = selection.EndLine; i < Lines.Count; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator) break;
|
||||
if (revert)
|
||||
{
|
||||
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
tail = line.Content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
tail = line.Content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the first line is not indicator.
|
||||
if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator)
|
||||
{
|
||||
var indicator = selection.StartLine - 1;
|
||||
for (int i = selection.StartLine - 2; i >= 0; i--)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator)
|
||||
{
|
||||
indicator = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var ignoreAdds = 0;
|
||||
var ignoreRemoves = 0;
|
||||
for (int i = 0; i < indicator; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
ignoreAdds++;
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
ignoreRemoves++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = indicator; i < selection.StartLine - 1; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator)
|
||||
{
|
||||
ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, tail != null);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
if (revert) builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
if (!revert) builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Normal)
|
||||
{
|
||||
builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs the selected lines.
|
||||
for (int i = selection.StartLine - 1; i < selection.EndLine; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator)
|
||||
{
|
||||
if (!ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, tail != null))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Normal)
|
||||
{
|
||||
builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
builder.Append("\n+").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
builder.Append("\n-").Append(line.Content);
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append("\n ").Append(tail);
|
||||
builder.Append("\n");
|
||||
System.IO.File.WriteAllText(output, builder.ToString());
|
||||
}
|
||||
|
||||
public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, bool isOldSide, string output)
|
||||
{
|
||||
var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
|
||||
builder.Append("index 00000000...").Append(fileTreeGuid).Append(" 100644\n");
|
||||
builder.Append("--- a/").Append(orgFile).Append('\n');
|
||||
builder.Append("+++ b/").Append(change.Path);
|
||||
|
||||
// If last line of selection is a change. Find one more line.
|
||||
var tail = null as string;
|
||||
if (selection.EndLine < Lines.Count)
|
||||
{
|
||||
var lastLine = Lines[selection.EndLine - 1];
|
||||
if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
for (int i = selection.EndLine; i < Lines.Count; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator) break;
|
||||
if (revert)
|
||||
{
|
||||
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
tail = line.Content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
tail = line.Content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the first line is not indicator.
|
||||
if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator)
|
||||
{
|
||||
var indicator = selection.StartLine - 1;
|
||||
for (int i = selection.StartLine - 2; i >= 0; i--)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator)
|
||||
{
|
||||
indicator = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var ignoreAdds = 0;
|
||||
var ignoreRemoves = 0;
|
||||
for (int i = 0; i < indicator; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
ignoreAdds++;
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
ignoreRemoves++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = indicator; i < selection.StartLine - 1; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator)
|
||||
{
|
||||
ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, isOldSide, tail != null);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
if (revert) builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
if (!revert) builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Normal)
|
||||
{
|
||||
builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs the selected lines.
|
||||
for (int i = selection.StartLine - 1; i < selection.EndLine; i++)
|
||||
{
|
||||
var line = Lines[i];
|
||||
if (line.Type == TextDiffLineType.Indicator)
|
||||
{
|
||||
if (!ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, isOldSide, tail != null))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Normal)
|
||||
{
|
||||
builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Added)
|
||||
{
|
||||
if (isOldSide)
|
||||
{
|
||||
if (revert)
|
||||
{
|
||||
builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
selection.IgnoredAdds++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("\n+").Append(line.Content);
|
||||
}
|
||||
}
|
||||
else if (line.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
if (isOldSide)
|
||||
{
|
||||
builder.Append("\n-").Append(line.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!revert)
|
||||
{
|
||||
builder.Append("\n ").Append(line.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
selection.IgnoredDeletes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append("\n ").Append(tail);
|
||||
builder.Append("\n");
|
||||
System.IO.File.WriteAllText(output, builder.ToString());
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
|
||||
private static partial Regex indicatorRegex();
|
||||
|
||||
private bool ProcessIndicatorForPatch(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed)
|
||||
{
|
||||
|
||||
|
||||
var match = indicatorRegex().Match(indicator.Content);
|
||||
var oldStart = int.Parse(match.Groups[1].Value);
|
||||
var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds;
|
||||
var oldCount = 0;
|
||||
var newCount = 0;
|
||||
for (int i = idx + 1; i < end; i++)
|
||||
{
|
||||
var test = Lines[i];
|
||||
if (test.Type == TextDiffLineType.Indicator) break;
|
||||
|
||||
if (test.Type == TextDiffLineType.Normal)
|
||||
{
|
||||
oldCount++;
|
||||
newCount++;
|
||||
}
|
||||
else if (test.Type == TextDiffLineType.Added)
|
||||
{
|
||||
if (i < start - 1)
|
||||
{
|
||||
if (revert)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newCount++;
|
||||
}
|
||||
|
||||
if (i == end - 1 && tailed)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else if (test.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
if (i < start - 1)
|
||||
{
|
||||
if (!revert)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
oldCount++;
|
||||
}
|
||||
|
||||
if (i == end - 1 && tailed)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldCount == 0 && newCount == 0) return false;
|
||||
|
||||
builder.Append($"\n@@ -{oldStart},{oldCount} +{newStart},{newCount} @@");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ProcessIndicatorForPatchSingleSide(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool isOldSide, bool tailed)
|
||||
{
|
||||
|
||||
var match = indicatorRegex().Match(indicator.Content);
|
||||
var oldStart = int.Parse(match.Groups[1].Value);
|
||||
var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds;
|
||||
var oldCount = 0;
|
||||
var newCount = 0;
|
||||
for (int i = idx + 1; i < end; i++)
|
||||
{
|
||||
var test = Lines[i];
|
||||
if (test.Type == TextDiffLineType.Indicator) break;
|
||||
|
||||
if (test.Type == TextDiffLineType.Normal)
|
||||
{
|
||||
oldCount++;
|
||||
newCount++;
|
||||
}
|
||||
else if (test.Type == TextDiffLineType.Added)
|
||||
{
|
||||
if (i < start - 1)
|
||||
{
|
||||
if (revert)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOldSide)
|
||||
{
|
||||
if (revert)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == end - 1 && tailed)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else if (test.Type == TextDiffLineType.Deleted)
|
||||
{
|
||||
if (i < start - 1)
|
||||
{
|
||||
if (!revert)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOldSide)
|
||||
{
|
||||
oldCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!revert)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i == end - 1 && tailed)
|
||||
{
|
||||
newCount++;
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldCount == 0 && newCount == 0) return false;
|
||||
|
||||
builder.Append($"\n@@ -{oldStart},{oldCount} +{newStart},{newCount} @@");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class LFSDiff
|
||||
{
|
||||
public LFSObject Old { get; set; } = new LFSObject();
|
||||
public LFSObject New { get; set; } = new LFSObject();
|
||||
}
|
||||
|
||||
public class BinaryDiff
|
||||
{
|
||||
public long OldSize { get; set; } = 0;
|
||||
public long NewSize { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class DiffResult
|
||||
{
|
||||
public bool IsBinary { get; set; } = false;
|
||||
public bool IsLFS { get; set; } = false;
|
||||
public TextDiff TextDiff { get; set; } = null;
|
||||
public LFSDiff LFSDiff { get; set; } = null;
|
||||
}
|
||||
}
|
66
src/SourceGit/Models/ExternalMergeTools.cs
Normal file
66
src/SourceGit/Models/ExternalMergeTools.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class ExternalMergeTools
|
||||
{
|
||||
public int Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Exec { get; set; }
|
||||
public string Cmd { get; set; }
|
||||
public string DiffCmd { get; set; }
|
||||
|
||||
public static List<ExternalMergeTools> Supported;
|
||||
|
||||
static ExternalMergeTools()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Supported = new List<ExternalMergeTools>() {
|
||||
new ExternalMergeTools(0, "Custom", "", "", ""),
|
||||
new ExternalMergeTools(1, "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(2, "Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" /m", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(3, "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\""),
|
||||
new ExternalMergeTools(4, "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(5, "Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(6, "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
|
||||
};
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Supported = new List<ExternalMergeTools>() {
|
||||
new ExternalMergeTools(0, "Custom", "", "", ""),
|
||||
new ExternalMergeTools(1, "FileMerge", "/usr/bin/opendiff", "\"$BASE\" \"$LOCAL\" \"$REMOTE\" -ancestor \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(2, "Visual Studio Code", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(3, "KDiff3", "/Applications/kdiff3.app/Contents/MacOS/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(4, "Beyond Compare 4", "/Applications/Beyond Compare.app/Contents/MacOS/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
};
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Supported = new List<ExternalMergeTools>() {
|
||||
new ExternalMergeTools(0, "Custom", "", "", ""),
|
||||
new ExternalMergeTools(1, "Visual Studio Code", "/usr/share/code/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(2, "KDiff3", "/usr/bin/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
new ExternalMergeTools(3, "Beyond Compare 4", "/usr/bin/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
Supported = new List<ExternalMergeTools>() {
|
||||
new ExternalMergeTools(0, "Custom", "", "", ""),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalMergeTools(int type, string name, string exec, string cmd, string diffCmd)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
Exec = exec;
|
||||
Cmd = cmd;
|
||||
DiffCmd = diffCmd;
|
||||
}
|
||||
}
|
||||
}
|
36
src/SourceGit/Models/GitFlow.cs
Normal file
36
src/SourceGit/Models/GitFlow.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public enum GitFlowBranchType
|
||||
{
|
||||
None,
|
||||
Feature,
|
||||
Release,
|
||||
Hotfix,
|
||||
}
|
||||
|
||||
public class GitFlow
|
||||
{
|
||||
public string Feature { get; set; }
|
||||
public string Release { get; set; }
|
||||
public string Hotfix { get; set; }
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(Feature)
|
||||
&& !string.IsNullOrEmpty(Release)
|
||||
&& !string.IsNullOrEmpty(Hotfix);
|
||||
}
|
||||
}
|
||||
|
||||
public GitFlowBranchType GetBranchType(string name)
|
||||
{
|
||||
if (!IsEnabled) return GitFlowBranchType.None;
|
||||
if (name.StartsWith(Feature)) return GitFlowBranchType.Feature;
|
||||
if (name.StartsWith(Release)) return GitFlowBranchType.Release;
|
||||
if (name.StartsWith(Hotfix)) return GitFlowBranchType.Hotfix;
|
||||
return GitFlowBranchType.None;
|
||||
}
|
||||
}
|
||||
}
|
8
src/SourceGit/Models/LFSObject.cs
Normal file
8
src/SourceGit/Models/LFSObject.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public class LFSObject
|
||||
{
|
||||
public string Oid { get; set; } = string.Empty;
|
||||
public long Size { get; set; } = 0;
|
||||
}
|
||||
}
|
21
src/SourceGit/Models/Locales.cs
Normal file
21
src/SourceGit/Models/Locales.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class Locale
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Key { get; set; }
|
||||
|
||||
public static List<Locale> Supported = new List<Locale>() {
|
||||
new Locale("English", "en_US"),
|
||||
new Locale("简体中文", "zh_CN"),
|
||||
};
|
||||
|
||||
public Locale(string name, string key)
|
||||
{
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
}
|
13
src/SourceGit/Models/Notification.cs
Normal file
13
src/SourceGit/Models/Notification.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public class Notification
|
||||
{
|
||||
public bool IsError { get; set; } = false;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public interface INotificationReceiver
|
||||
{
|
||||
void OnReceiveNotification(string ctx, Notification notice);
|
||||
}
|
||||
}
|
18
src/SourceGit/Models/Object.cs
Normal file
18
src/SourceGit/Models/Object.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public enum ObjectType
|
||||
{
|
||||
None,
|
||||
Blob,
|
||||
Tree,
|
||||
Tag,
|
||||
Commit,
|
||||
}
|
||||
|
||||
public class Object
|
||||
{
|
||||
public string SHA { get; set; }
|
||||
public ObjectType Type { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
}
|
46
src/SourceGit/Models/Remote.cs
Normal file
46
src/SourceGit/Models/Remote.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public partial class Remote
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$")]
|
||||
private static partial Regex regex1();
|
||||
|
||||
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-\.]+\.git$")]
|
||||
private static partial Regex regex2();
|
||||
[GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$")]
|
||||
private static partial Regex regex3();
|
||||
|
||||
private static readonly Regex[] URL_FORMATS = [
|
||||
regex1(),
|
||||
regex2(),
|
||||
regex3(),
|
||||
];
|
||||
|
||||
public string Name { get; set; }
|
||||
public string URL { get; set; }
|
||||
|
||||
public static bool IsSSH(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return false;
|
||||
|
||||
for (int i = 1; i < URL_FORMATS.Length; i++)
|
||||
{
|
||||
if (URL_FORMATS[i].IsMatch(url)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsValidURL(string url)
|
||||
{
|
||||
foreach (var fmt in URL_FORMATS)
|
||||
{
|
||||
if (fmt.IsMatch(url)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
23
src/SourceGit/Models/RevisionFile.cs
Normal file
23
src/SourceGit/Models/RevisionFile.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public class RevisionBinaryFile
|
||||
{
|
||||
public long Size { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class RevisionTextFile
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
public class RevisionLFSObject
|
||||
{
|
||||
public LFSObject Object { get; set; }
|
||||
}
|
||||
|
||||
public class RevisionSubmodule
|
||||
{
|
||||
public string SHA { get; set; }
|
||||
}
|
||||
}
|
17
src/SourceGit/Models/Stash.cs
Normal file
17
src/SourceGit/Models/Stash.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class Stash
|
||||
{
|
||||
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
public string Name { get; set; } = "";
|
||||
public string SHA { get; set; } = "";
|
||||
public User Author { get; set; } = User.Invalid;
|
||||
public ulong Time { get; set; } = 0;
|
||||
public string Message { get; set; } = "";
|
||||
|
||||
public string TimeStr => UTC_START.AddSeconds(Time).ToString("yyyy/MM/dd HH:mm:ss");
|
||||
}
|
||||
}
|
144
src/SourceGit/Models/Statistics.cs
Normal file
144
src/SourceGit/Models/Statistics.cs
Normal file
|
@ -0,0 +1,144 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class StatisticsSample
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class StatisticsReport
|
||||
{
|
||||
public int Total { get; set; } = 0;
|
||||
public List<StatisticsSample> Samples { get; set; } = new List<StatisticsSample>();
|
||||
public List<StatisticsSample> ByCommitter { get; set; } = new List<StatisticsSample>();
|
||||
|
||||
public void AddCommit(int index, string committer)
|
||||
{
|
||||
Total++;
|
||||
Samples[index].Count++;
|
||||
|
||||
if (_mapByCommitter.ContainsKey(committer))
|
||||
{
|
||||
_mapByCommitter[committer].Count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sample = new StatisticsSample() { Name = committer, Count = 1 };
|
||||
_mapByCommitter.Add(committer, sample);
|
||||
ByCommitter.Add(sample);
|
||||
}
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
ByCommitter.Sort((l, r) => r.Count - l.Count);
|
||||
_mapByCommitter.Clear();
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, StatisticsSample> _mapByCommitter = new Dictionary<string, StatisticsSample>();
|
||||
}
|
||||
|
||||
public class Statistics
|
||||
{
|
||||
public StatisticsReport Year { get; set; } = new StatisticsReport();
|
||||
public StatisticsReport Month { get; set; } = new StatisticsReport();
|
||||
public StatisticsReport Week { get; set; } = new StatisticsReport();
|
||||
|
||||
public Statistics()
|
||||
{
|
||||
_utcStart = DateTime.UnixEpoch;
|
||||
_today = DateTime.Today;
|
||||
_thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second);
|
||||
_thisWeekEnd = _thisWeekStart.AddDays(7);
|
||||
|
||||
string[] monthNames = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
for (int i = 0; i < monthNames.Length; i++)
|
||||
{
|
||||
Year.Samples.Add(new StatisticsSample
|
||||
{
|
||||
Name = monthNames[i],
|
||||
Count = 0,
|
||||
});
|
||||
}
|
||||
|
||||
var monthDays = DateTime.DaysInMonth(_today.Year, _today.Month);
|
||||
for (int i = 0; i < monthDays; i++)
|
||||
{
|
||||
Month.Samples.Add(new StatisticsSample
|
||||
{
|
||||
Name = $"{i + 1}",
|
||||
Count = 0,
|
||||
});
|
||||
}
|
||||
|
||||
string[] weekDayNames = [
|
||||
"SUN",
|
||||
"MON",
|
||||
"TUE",
|
||||
"WED",
|
||||
"THU",
|
||||
"FRI",
|
||||
"SAT",
|
||||
];
|
||||
|
||||
for (int i = 0; i < weekDayNames.Length; i++)
|
||||
{
|
||||
Week.Samples.Add(new StatisticsSample
|
||||
{
|
||||
Name = weekDayNames[i],
|
||||
Count = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public string Since()
|
||||
{
|
||||
return _today.ToString("yyyy-01-01 00:00:00");
|
||||
}
|
||||
|
||||
public void AddCommit(string committer, double timestamp)
|
||||
{
|
||||
var time = _utcStart.AddSeconds(timestamp).ToLocalTime();
|
||||
if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0)
|
||||
{
|
||||
Week.AddCommit((int)time.DayOfWeek, committer);
|
||||
}
|
||||
|
||||
if (time.Month == _today.Month)
|
||||
{
|
||||
Month.AddCommit(time.Day - 1, committer);
|
||||
}
|
||||
|
||||
Year.AddCommit(time.Month - 1, committer);
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
Year.Complete();
|
||||
Month.Complete();
|
||||
Week.Complete();
|
||||
}
|
||||
|
||||
private readonly DateTime _utcStart;
|
||||
private readonly DateTime _today;
|
||||
private readonly DateTime _thisWeekStart;
|
||||
private readonly DateTime _thisWeekEnd;
|
||||
}
|
||||
}
|
9
src/SourceGit/Models/Tag.cs
Normal file
9
src/SourceGit/Models/Tag.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace SourceGit.Models
|
||||
{
|
||||
public class Tag
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string SHA { get; set; }
|
||||
public bool IsFiltered { get; set; }
|
||||
}
|
||||
}
|
320
src/SourceGit/Models/TextInlineChange.cs
Normal file
320
src/SourceGit/Models/TextInlineChange.cs
Normal file
|
@ -0,0 +1,320 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class TextInlineChange
|
||||
{
|
||||
public int DeletedStart { get; set; }
|
||||
public int DeletedCount { get; set; }
|
||||
public int AddedStart { get; set; }
|
||||
public int AddedCount { get; set; }
|
||||
|
||||
class Chunk
|
||||
{
|
||||
public int Hash;
|
||||
public bool Modified;
|
||||
public int Start;
|
||||
public int Size;
|
||||
|
||||
public Chunk(int hash, int start, int size)
|
||||
{
|
||||
Hash = hash;
|
||||
Modified = false;
|
||||
Start = start;
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
|
||||
enum Edit
|
||||
{
|
||||
None,
|
||||
DeletedRight,
|
||||
DeletedLeft,
|
||||
AddedRight,
|
||||
AddedLeft,
|
||||
}
|
||||
|
||||
class EditResult
|
||||
{
|
||||
public Edit State;
|
||||
public int DeleteStart;
|
||||
public int DeleteEnd;
|
||||
public int AddStart;
|
||||
public int AddEnd;
|
||||
}
|
||||
|
||||
public TextInlineChange(int dp, int dc, int ap, int ac)
|
||||
{
|
||||
DeletedStart = dp;
|
||||
DeletedCount = dc;
|
||||
AddedStart = ap;
|
||||
AddedCount = ac;
|
||||
}
|
||||
|
||||
public static List<TextInlineChange> Compare(string oldValue, string newValue)
|
||||
{
|
||||
var hashes = new Dictionary<string, int>();
|
||||
var chunksOld = MakeChunks(hashes, oldValue);
|
||||
var chunksNew = MakeChunks(hashes, newValue);
|
||||
var sizeOld = chunksOld.Count;
|
||||
var sizeNew = chunksNew.Count;
|
||||
var max = sizeOld + sizeNew + 2;
|
||||
var forward = new int[max];
|
||||
var reverse = new int[max];
|
||||
CheckModified(chunksOld, 0, sizeOld, chunksNew, 0, sizeNew, forward, reverse);
|
||||
|
||||
var ret = new List<TextInlineChange>();
|
||||
var posOld = 0;
|
||||
var posNew = 0;
|
||||
var last = null as TextInlineChange;
|
||||
do
|
||||
{
|
||||
while (posOld < sizeOld && posNew < sizeNew && !chunksOld[posOld].Modified && !chunksNew[posNew].Modified)
|
||||
{
|
||||
posOld++;
|
||||
posNew++;
|
||||
}
|
||||
|
||||
var beginOld = posOld;
|
||||
var beginNew = posNew;
|
||||
var countOld = 0;
|
||||
var countNew = 0;
|
||||
for (; posOld < sizeOld && chunksOld[posOld].Modified; posOld++) countOld += chunksOld[posOld].Size;
|
||||
for (; posNew < sizeNew && chunksNew[posNew].Modified; posNew++) countNew += chunksNew[posNew].Size;
|
||||
|
||||
if (countOld + countNew == 0) continue;
|
||||
|
||||
var diff = new TextInlineChange(
|
||||
countOld > 0 ? chunksOld[beginOld].Start : 0,
|
||||
countOld,
|
||||
countNew > 0 ? chunksNew[beginNew].Start : 0,
|
||||
countNew);
|
||||
if (last != null)
|
||||
{
|
||||
var midSizeOld = diff.DeletedStart - last.DeletedStart - last.DeletedCount;
|
||||
var midSizeNew = diff.AddedStart - last.AddedStart - last.AddedCount;
|
||||
if (midSizeOld == 1 && midSizeNew == 1)
|
||||
{
|
||||
last.DeletedCount += (1 + countOld);
|
||||
last.AddedCount += (1 + countNew);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
last = diff;
|
||||
ret.Add(diff);
|
||||
} while (posOld < sizeOld && posNew < sizeNew);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static List<Chunk> MakeChunks(Dictionary<string, int> hashes, string text)
|
||||
{
|
||||
var start = 0;
|
||||
var size = text.Length;
|
||||
var chunks = new List<Chunk>();
|
||||
var delims = new HashSet<char>(" \t+-*/=!,:;.'\"/?|&#@%`<>()[]{}\\".ToCharArray());
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
var ch = text[i];
|
||||
if (delims.Contains(ch))
|
||||
{
|
||||
if (start != i) AddChunk(chunks, hashes, text.Substring(start, i - start), start);
|
||||
AddChunk(chunks, hashes, text.Substring(i, 1), i);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < size) AddChunk(chunks, hashes, text.Substring(start), start);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static void CheckModified(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse)
|
||||
{
|
||||
while (startOld < endOld && startNew < endNew && chunksOld[startOld].Hash == chunksNew[startNew].Hash)
|
||||
{
|
||||
startOld++;
|
||||
startNew++;
|
||||
}
|
||||
|
||||
while (startOld < endOld && startNew < endNew && chunksOld[endOld - 1].Hash == chunksNew[endNew - 1].Hash)
|
||||
{
|
||||
endOld--;
|
||||
endNew--;
|
||||
}
|
||||
|
||||
var lenOld = endOld - startOld;
|
||||
var lenNew = endNew - startNew;
|
||||
if (lenOld > 0 && lenNew > 0)
|
||||
{
|
||||
var rs = CheckModifiedEdit(chunksOld, startOld, endOld, chunksNew, startNew, endNew, forward, reverse);
|
||||
if (rs.State == Edit.None) return;
|
||||
|
||||
if (rs.State == Edit.DeletedRight && rs.DeleteStart - 1 > startOld)
|
||||
{
|
||||
chunksOld[--rs.DeleteStart].Modified = true;
|
||||
}
|
||||
else if (rs.State == Edit.DeletedLeft && rs.DeleteEnd < endOld)
|
||||
{
|
||||
chunksOld[rs.DeleteEnd++].Modified = true;
|
||||
}
|
||||
else if (rs.State == Edit.AddedRight && rs.AddStart - 1 > startNew)
|
||||
{
|
||||
chunksNew[--rs.AddStart].Modified = true;
|
||||
}
|
||||
else if (rs.State == Edit.AddedLeft && rs.AddEnd < endNew)
|
||||
{
|
||||
chunksNew[rs.AddEnd++].Modified = true;
|
||||
}
|
||||
|
||||
CheckModified(chunksOld, startOld, rs.DeleteStart, chunksNew, startNew, rs.AddStart, forward, reverse);
|
||||
CheckModified(chunksOld, rs.DeleteEnd, endOld, chunksNew, rs.AddEnd, endNew, forward, reverse);
|
||||
}
|
||||
else if (lenOld > 0)
|
||||
{
|
||||
for (int i = startOld; i < endOld; i++) chunksOld[i].Modified = true;
|
||||
}
|
||||
else if (lenNew > 0)
|
||||
{
|
||||
for (int i = startNew; i < endNew; i++) chunksNew[i].Modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static EditResult CheckModifiedEdit(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse)
|
||||
{
|
||||
var lenOld = endOld - startOld;
|
||||
var lenNew = endNew - startNew;
|
||||
var max = lenOld + lenNew + 1;
|
||||
var half = max / 2;
|
||||
var delta = lenOld - lenNew;
|
||||
var deltaEven = delta % 2 == 0;
|
||||
var rs = new EditResult() { State = Edit.None };
|
||||
|
||||
forward[1 + half] = 0;
|
||||
reverse[1 + half] = lenOld + 1;
|
||||
|
||||
for (int i = 0; i <= half; i++)
|
||||
{
|
||||
|
||||
for (int j = -i; j <= i; j += 2)
|
||||
{
|
||||
var idx = j + half;
|
||||
int o, n;
|
||||
if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1]))
|
||||
{
|
||||
o = forward[idx + 1];
|
||||
rs.State = Edit.AddedRight;
|
||||
}
|
||||
else
|
||||
{
|
||||
o = forward[idx - 1] + 1;
|
||||
rs.State = Edit.DeletedRight;
|
||||
}
|
||||
|
||||
n = o - j;
|
||||
|
||||
var startX = o;
|
||||
var startY = n;
|
||||
while (o < lenOld && n < lenNew && chunksOld[o + startOld].Hash == chunksNew[n + startNew].Hash)
|
||||
{
|
||||
o++;
|
||||
n++;
|
||||
}
|
||||
|
||||
forward[idx] = o;
|
||||
|
||||
if (!deltaEven && j - delta >= -i + 1 && j - delta <= i - 1)
|
||||
{
|
||||
var revIdx = (j - delta) + half;
|
||||
var revOld = reverse[revIdx];
|
||||
int revNew = revOld - j;
|
||||
if (revOld <= o && revNew <= n)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
rs.State = Edit.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.DeleteStart = startX + startOld;
|
||||
rs.DeleteEnd = o + startOld;
|
||||
rs.AddStart = startY + startNew;
|
||||
rs.AddEnd = n + startNew;
|
||||
}
|
||||
return rs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = -i; j <= i; j += 2)
|
||||
{
|
||||
var idx = j + half;
|
||||
int o, n;
|
||||
if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1]))
|
||||
{
|
||||
o = reverse[idx + 1] - 1;
|
||||
rs.State = Edit.DeletedLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
o = reverse[idx - 1];
|
||||
rs.State = Edit.AddedLeft;
|
||||
}
|
||||
|
||||
n = o - (j + delta);
|
||||
|
||||
var endX = o;
|
||||
var endY = n;
|
||||
while (o > 0 && n > 0 && chunksOld[startOld + o - 1].Hash == chunksNew[startNew + n - 1].Hash)
|
||||
{
|
||||
o--;
|
||||
n--;
|
||||
}
|
||||
|
||||
reverse[idx] = o;
|
||||
|
||||
if (deltaEven && j + delta >= -i && j + delta <= i)
|
||||
{
|
||||
var forIdx = (j + delta) + half;
|
||||
var forOld = forward[forIdx];
|
||||
int forNew = forOld - (j + delta);
|
||||
if (forOld >= o && forNew >= n)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
rs.State = Edit.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.DeleteStart = o + startOld;
|
||||
rs.DeleteEnd = endX + startOld;
|
||||
rs.AddStart = n + startNew;
|
||||
rs.AddEnd = endY + startNew;
|
||||
}
|
||||
return rs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rs.State = Edit.None;
|
||||
return rs;
|
||||
}
|
||||
|
||||
private static void AddChunk(List<Chunk> chunks, Dictionary<string, int> hashes, string data, int start)
|
||||
{
|
||||
int hash;
|
||||
if (hashes.TryGetValue(data, out hash))
|
||||
{
|
||||
chunks.Add(new Chunk(hash, start, data.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
hash = hashes.Count;
|
||||
hashes.Add(data, hash);
|
||||
chunks.Add(new Chunk(hash, start, data.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
src/SourceGit/Models/User.cs
Normal file
44
src/SourceGit/Models/User.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public static User Invalid = new User();
|
||||
public static Dictionary<string, User> Caches = new Dictionary<string, User>();
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj == null || !(obj is User)) return false;
|
||||
|
||||
var other = obj as User;
|
||||
return Name == other.Name && Email == other.Email;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public static User FindOrAdd(string data)
|
||||
{
|
||||
if (Caches.ContainsKey(data))
|
||||
{
|
||||
return Caches[data];
|
||||
}
|
||||
else
|
||||
{
|
||||
var nameEndIdx = data.IndexOf('<', System.StringComparison.Ordinal);
|
||||
var name = nameEndIdx >= 2 ? data.Substring(0, nameEndIdx - 1) : string.Empty;
|
||||
var email = data.Substring(nameEndIdx + 1);
|
||||
|
||||
User user = new User() { Name = name, Email = email };
|
||||
Caches.Add(data, user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
204
src/SourceGit/Models/Watcher.cs
Normal file
204
src/SourceGit/Models/Watcher.cs
Normal file
|
@ -0,0 +1,204 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public interface IRepository
|
||||
{
|
||||
string FullPath { get; set; }
|
||||
string GitDir { get; set; }
|
||||
|
||||
void RefreshBranches();
|
||||
void RefreshTags();
|
||||
void RefreshCommits();
|
||||
void RefreshSubmodules();
|
||||
void RefreshWorkingCopyChanges();
|
||||
void RefreshStashes();
|
||||
}
|
||||
|
||||
public class Watcher : IDisposable
|
||||
{
|
||||
public Watcher(IRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
|
||||
_wcWatcher = new FileSystemWatcher();
|
||||
_wcWatcher.Path = _repo.FullPath;
|
||||
_wcWatcher.Filter = "*";
|
||||
_wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime;
|
||||
_wcWatcher.IncludeSubdirectories = true;
|
||||
_wcWatcher.Created += OnWorkingCopyChanged;
|
||||
_wcWatcher.Renamed += OnWorkingCopyChanged;
|
||||
_wcWatcher.Changed += OnWorkingCopyChanged;
|
||||
_wcWatcher.Deleted += OnWorkingCopyChanged;
|
||||
_wcWatcher.EnableRaisingEvents = true;
|
||||
|
||||
_repoWatcher = new FileSystemWatcher();
|
||||
_repoWatcher.Path = _repo.GitDir;
|
||||
_repoWatcher.Filter = "*";
|
||||
_repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;
|
||||
_repoWatcher.IncludeSubdirectories = true;
|
||||
_repoWatcher.Created += OnRepositoryChanged;
|
||||
_repoWatcher.Renamed += OnRepositoryChanged;
|
||||
_repoWatcher.Changed += OnRepositoryChanged;
|
||||
_repoWatcher.Deleted += OnRepositoryChanged;
|
||||
_repoWatcher.EnableRaisingEvents = true;
|
||||
|
||||
_timer = new Timer(Tick, null, 100, 100);
|
||||
}
|
||||
|
||||
public void SetEnabled(bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
if (_lockCount > 0) _lockCount--;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lockCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkBranchDirtyManually()
|
||||
{
|
||||
_updateBranch = DateTime.Now.ToFileTime() - 1;
|
||||
}
|
||||
|
||||
public void MarkWorkingCopyDirtyManually()
|
||||
{
|
||||
_updateWC = DateTime.Now.ToFileTime() - 1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_repoWatcher.EnableRaisingEvents = false;
|
||||
_repoWatcher.Created -= OnRepositoryChanged;
|
||||
_repoWatcher.Renamed -= OnRepositoryChanged;
|
||||
_repoWatcher.Changed -= OnRepositoryChanged;
|
||||
_repoWatcher.Deleted -= OnRepositoryChanged;
|
||||
_repoWatcher.Dispose();
|
||||
_repoWatcher = null;
|
||||
|
||||
_wcWatcher.EnableRaisingEvents = false;
|
||||
_wcWatcher.Created -= OnWorkingCopyChanged;
|
||||
_wcWatcher.Renamed -= OnWorkingCopyChanged;
|
||||
_wcWatcher.Changed -= OnWorkingCopyChanged;
|
||||
_wcWatcher.Deleted -= OnWorkingCopyChanged;
|
||||
_wcWatcher.Dispose();
|
||||
_wcWatcher = null;
|
||||
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
private void Tick(object sender)
|
||||
{
|
||||
if (_lockCount > 0) return;
|
||||
|
||||
var now = DateTime.Now.ToFileTime();
|
||||
if (_updateBranch > 0 && now > _updateBranch)
|
||||
{
|
||||
_updateBranch = 0;
|
||||
_updateWC = 0;
|
||||
|
||||
if (_updateTags > 0)
|
||||
{
|
||||
_updateTags = 0;
|
||||
Task.Run(() =>
|
||||
{
|
||||
_repo.RefreshTags();
|
||||
_repo.RefreshBranches();
|
||||
_repo.RefreshCommits();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
_repo.RefreshBranches();
|
||||
_repo.RefreshCommits();
|
||||
});
|
||||
}
|
||||
|
||||
Task.Run(_repo.RefreshWorkingCopyChanges);
|
||||
}
|
||||
|
||||
if (_updateWC > 0 && now > _updateWC)
|
||||
{
|
||||
_updateWC = 0;
|
||||
Task.Run(_repo.RefreshWorkingCopyChanges);
|
||||
}
|
||||
|
||||
if (_updateSubmodules > 0 && now > _updateSubmodules)
|
||||
{
|
||||
_updateSubmodules = 0;
|
||||
_repo.RefreshSubmodules();
|
||||
}
|
||||
|
||||
if (_updateStashes > 0 && now > _updateStashes)
|
||||
{
|
||||
_updateStashes = 0;
|
||||
_repo.RefreshStashes();
|
||||
}
|
||||
|
||||
if (_updateTags > 0 && now > _updateTags)
|
||||
{
|
||||
_updateTags = 0;
|
||||
_repo.RefreshTags();
|
||||
_repo.RefreshCommits();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRepositoryChanged(object o, FileSystemEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Name)) return;
|
||||
|
||||
var name = e.Name.Replace("\\", "/");
|
||||
if (name.StartsWith("modules", StringComparison.Ordinal))
|
||||
{
|
||||
_updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
}
|
||||
else if (name.StartsWith("refs/tags", StringComparison.Ordinal))
|
||||
{
|
||||
_updateTags = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
}
|
||||
else if (name.StartsWith("refs/stash", StringComparison.Ordinal))
|
||||
{
|
||||
_updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
}
|
||||
else if (name.Equals("HEAD", StringComparison.Ordinal) ||
|
||||
name.StartsWith("refs/heads/", StringComparison.Ordinal) ||
|
||||
name.StartsWith("refs/remotes/", StringComparison.Ordinal) ||
|
||||
name.StartsWith("worktrees/", StringComparison.Ordinal))
|
||||
{
|
||||
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
}
|
||||
else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal))
|
||||
{
|
||||
_updateWC = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWorkingCopyChanged(object o, FileSystemEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Name)) return;
|
||||
|
||||
var name = e.Name.Replace("\\", "/");
|
||||
if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal)) return;
|
||||
_updateWC = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
}
|
||||
|
||||
private readonly IRepository _repo = null;
|
||||
private FileSystemWatcher _repoWatcher = null;
|
||||
private FileSystemWatcher _wcWatcher = null;
|
||||
private Timer _timer = null;
|
||||
private int _lockCount = 0;
|
||||
private long _updateWC = 0;
|
||||
private long _updateBranch = 0;
|
||||
private long _updateSubmodules = 0;
|
||||
private long _updateStashes = 0;
|
||||
private long _updateTags = 0;
|
||||
}
|
||||
}
|
108
src/SourceGit/Native/Linux.cs
Normal file
108
src/SourceGit/Native/Linux.cs
Normal file
|
@ -0,0 +1,108 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Dialogs;
|
||||
|
||||
namespace SourceGit.Native
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
internal class Linux : OS.IBackend
|
||||
{
|
||||
public void SetupApp(AppBuilder builder)
|
||||
{
|
||||
#if USE_FONT_INTER
|
||||
builder.WithInterFont();
|
||||
#endif
|
||||
// Free-desktop file picker has an extra black background panel.
|
||||
builder.UseManagedSystemDialogs();
|
||||
}
|
||||
|
||||
public string FindGitExecutable()
|
||||
{
|
||||
if (File.Exists("/usr/bin/git")) return "/usr/bin/git";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string FindVSCode()
|
||||
{
|
||||
if (File.Exists("/usr/share/code/code")) return "/usr/share/code/code";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public void OpenBrowser(string url)
|
||||
{
|
||||
if (!File.Exists("/usr/bin/xdg-open"))
|
||||
{
|
||||
App.RaiseException("", $"You should install xdg-open first!");
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start("xdg-open", $"\"{url}\"");
|
||||
}
|
||||
|
||||
public void OpenInFileManager(string path, bool select)
|
||||
{
|
||||
if (!File.Exists("/usr/bin/xdg-open"))
|
||||
{
|
||||
App.RaiseException("", $"You should install xdg-open first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Process.Start("xdg-open", $"\"{path}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
Process.Start("xdg-open", $"\"{dir}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenTerminal(string workdir)
|
||||
{
|
||||
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
|
||||
if (File.Exists("/usr/bin/gnome-terminal"))
|
||||
{
|
||||
Process.Start("/usr/bin/gnome-terminal", $"--working-directory=\"{dir}\"");
|
||||
}
|
||||
else if (File.Exists("/usr/bin/konsole"))
|
||||
{
|
||||
Process.Start("/usr/bin/konsole", $"--workdir \"{dir}\"");
|
||||
}
|
||||
else if (File.Exists("/usr/bin/xfce4-terminal"))
|
||||
{
|
||||
Process.Start("/usr/bin/xfce4-terminal", $"--working-directory=\"{dir}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
App.RaiseException("", $"Only supports gnome-terminal/konsole/xfce4-terminal!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenWithDefaultEditor(string file)
|
||||
{
|
||||
if (!File.Exists("/usr/bin/xdg-open"))
|
||||
{
|
||||
App.RaiseException("", $"You should install xdg-open first!");
|
||||
return;
|
||||
}
|
||||
|
||||
var proc = Process.Start("xdg-open", $"\"{file}\"");
|
||||
proc.WaitForExit();
|
||||
|
||||
if (proc.ExitCode != 0)
|
||||
{
|
||||
App.RaiseException("", $"Failed to open \"{file}\"");
|
||||
}
|
||||
|
||||
proc.Close();
|
||||
}
|
||||
}
|
||||
}
|
81
src/SourceGit/Native/MacOS.cs
Normal file
81
src/SourceGit/Native/MacOS.cs
Normal file
|
@ -0,0 +1,81 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.Native
|
||||
{
|
||||
[SupportedOSPlatform("macOS")]
|
||||
internal class MacOS : OS.IBackend
|
||||
{
|
||||
public void SetupApp(AppBuilder builder)
|
||||
{
|
||||
builder.With(new FontManagerOptions()
|
||||
{
|
||||
DefaultFamilyName = "PingFang SC",
|
||||
FontFallbacks = [
|
||||
new FontFallback { FontFamily = new FontFamily("PingFang SC") }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
public string FindGitExecutable()
|
||||
{
|
||||
if (File.Exists("/usr/bin/git")) return "/usr/bin/git";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string FindVSCode()
|
||||
{
|
||||
if (File.Exists("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"))
|
||||
{
|
||||
return "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public void OpenBrowser(string url)
|
||||
{
|
||||
Process.Start("open", url);
|
||||
}
|
||||
|
||||
public void OpenInFileManager(string path, bool select)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Process.Start("open", path);
|
||||
}
|
||||
else if (File.Exists(path))
|
||||
{
|
||||
Process.Start("open", $"\"{path}\" -R");
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenTerminal(string workdir)
|
||||
{
|
||||
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("on run argv");
|
||||
builder.AppendLine(" tell application \"Terminal\"");
|
||||
builder.AppendLine($" do script \"cd '{dir}'\"");
|
||||
builder.AppendLine(" activate");
|
||||
builder.AppendLine(" end tell");
|
||||
builder.AppendLine("end run");
|
||||
|
||||
var tmp = Path.GetTempFileName();
|
||||
File.WriteAllText(tmp, builder.ToString());
|
||||
|
||||
var proc = Process.Start("/usr/bin/osascript", $"\"{tmp}\"");
|
||||
proc.Exited += (o, e) => File.Delete(tmp);
|
||||
}
|
||||
|
||||
public void OpenWithDefaultEditor(string file)
|
||||
{
|
||||
Process.Start("open", file);
|
||||
}
|
||||
}
|
||||
}
|
107
src/SourceGit/Native/OS.cs
Normal file
107
src/SourceGit/Native/OS.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
using Avalonia;
|
||||
|
||||
namespace SourceGit.Native
|
||||
{
|
||||
public static class OS
|
||||
{
|
||||
public interface IBackend
|
||||
{
|
||||
void SetupApp(AppBuilder builder);
|
||||
|
||||
string FindGitExecutable();
|
||||
string FindVSCode();
|
||||
|
||||
void OpenTerminal(string workdir);
|
||||
void OpenInFileManager(string path, bool select);
|
||||
void OpenBrowser(string url);
|
||||
void OpenWithDefaultEditor(string file);
|
||||
}
|
||||
|
||||
public static string GitInstallPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public static string VSCodeExecutableFile
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
static OS()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_backend = new MacOS();
|
||||
VSCodeExecutableFile = _backend.FindVSCode();
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_backend = new Windows();
|
||||
VSCodeExecutableFile = _backend.FindVSCode();
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
_backend = new Linux();
|
||||
VSCodeExecutableFile = _backend.FindVSCode();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Platform unsupported!!!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetupApp(AppBuilder builder)
|
||||
{
|
||||
_backend?.SetupApp(builder);
|
||||
}
|
||||
|
||||
public static string FindGitExecutable()
|
||||
{
|
||||
return _backend?.FindGitExecutable();
|
||||
}
|
||||
|
||||
public static void OpenInFileManager(string path, bool select = false)
|
||||
{
|
||||
_backend?.OpenInFileManager(path, select);
|
||||
}
|
||||
|
||||
public static void OpenBrowser(string url)
|
||||
{
|
||||
_backend?.OpenBrowser(url);
|
||||
}
|
||||
|
||||
public static void OpenTerminal(string workdir)
|
||||
{
|
||||
_backend?.OpenTerminal(workdir);
|
||||
}
|
||||
|
||||
public static void OpenWithDefaultEditor(string file)
|
||||
{
|
||||
_backend?.OpenWithDefaultEditor(file);
|
||||
}
|
||||
|
||||
public static void OpenInVSCode(string repo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(VSCodeExecutableFile))
|
||||
{
|
||||
App.RaiseException(repo, "Visual Studio Code can NOT be found in your system!!!");
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo()
|
||||
{
|
||||
WorkingDirectory = repo,
|
||||
FileName = VSCodeExecutableFile,
|
||||
Arguments = $"\"{repo}\"",
|
||||
UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly IBackend _backend = null;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue