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

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