Project Location

This commit is contained in:
Enner Pérez 2024-03-20 02:36:10 -05:00
parent 014e37e505
commit a1a14f8858
305 changed files with 9783 additions and 9783 deletions

21
src/SourceGit/App.axaml Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

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

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

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

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

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

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

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

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

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

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

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

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

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