mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-21 20:24:59 +00:00
refactor<*>: rewrite all with AvaloniaUI
This commit is contained in:
parent
0136904612
commit
2a62596999
521 changed files with 19780 additions and 23244 deletions
13
src/Models/ApplyWhiteSpaceMode.cs
Normal file
13
src/Models/ApplyWhiteSpaceMode.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Models {
|
||||
public class ApplyWhiteSpaceMode {
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
|
||||
public ApplyWhiteSpaceMode(string n, string d, string a) {
|
||||
Name = App.Text(n);
|
||||
Desc = App.Text(d);
|
||||
Arg = a;
|
||||
}
|
||||
}
|
||||
}
|
120
src/Models/AvatarManager.cs
Normal file
120
src/Models/AvatarManager.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public interface IAvatarHost {
|
||||
void OnAvatarResourceReady(string md5, Bitmap bitmap);
|
||||
}
|
||||
|
||||
public static class AvatarManager {
|
||||
static AvatarManager() {
|
||||
_storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit", "avatars");
|
||||
if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath);
|
||||
|
||||
Task.Run(() => {
|
||||
while (true) {
|
||||
var md5 = null as string;
|
||||
|
||||
lock (_synclock) {
|
||||
foreach (var one in _requesting) {
|
||||
md5 = one;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (md5 == null) {
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
var localFile = Path.Combine(_storePath, md5);
|
||||
var img = null as Bitmap;
|
||||
try {
|
||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) };
|
||||
var task = client.GetAsync($"https://cravatar.cn/avatar/{md5}?d=404");
|
||||
task.Wait();
|
||||
|
||||
var rsp = task.Result;
|
||||
if (rsp.IsSuccessStatusCode) {
|
||||
using (var stream = rsp.Content.ReadAsStream()) {
|
||||
using (var writer = File.OpenWrite(localFile)) {
|
||||
stream.CopyTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
using (var reader = File.OpenRead(localFile)) {
|
||||
img = Bitmap.DecodeToWidth(reader, 128);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
lock (_synclock) {
|
||||
_requesting.Remove(md5);
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() => {
|
||||
if (_resources.ContainsKey(md5)) _resources[md5] = img;
|
||||
else _resources.Add(md5, img);
|
||||
if (img != null) NotifyResourceReady(md5, img);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void Subscribe(IAvatarHost host) {
|
||||
_avatars.Add(new WeakReference<IAvatarHost>(host));
|
||||
}
|
||||
|
||||
public static Bitmap Request(string md5, bool forceRefetch = false) {
|
||||
if (forceRefetch) {
|
||||
if (_resources.ContainsKey(md5)) _resources.Remove(md5);
|
||||
} else {
|
||||
if (_resources.ContainsKey(md5)) return _resources[md5];
|
||||
|
||||
var localFile = Path.Combine(_storePath, md5);
|
||||
if (File.Exists(localFile)) {
|
||||
try {
|
||||
using (var stream = File.OpenRead(localFile)) {
|
||||
var img = Bitmap.DecodeToWidth(stream, 128);
|
||||
_resources.Add(md5, img);
|
||||
return img;
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
lock (_synclock) {
|
||||
if (!_requesting.Contains(md5)) _requesting.Add(md5);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void NotifyResourceReady(string md5, Bitmap bitmap) {
|
||||
List<WeakReference<IAvatarHost>> invalids = new List<WeakReference<IAvatarHost>>();
|
||||
foreach (var avatar in _avatars) {
|
||||
IAvatarHost retrived = null;
|
||||
if (avatar.TryGetTarget(out retrived)) {
|
||||
retrived.OnAvatarResourceReady(md5, bitmap);
|
||||
break;
|
||||
} else {
|
||||
invalids.Add(avatar);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var invalid in invalids) _avatars.Remove(invalid);
|
||||
}
|
||||
|
||||
private static object _synclock = new object();
|
||||
private static string _storePath = string.Empty;
|
||||
private static List<WeakReference<IAvatarHost>> _avatars = new List<WeakReference<IAvatarHost>>();
|
||||
private static Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
|
||||
private static HashSet<string> _requesting = new HashSet<string>();
|
||||
}
|
||||
}
|
17
src/Models/Blame.cs
Normal file
17
src/Models/Blame.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class BlameLineInfo {
|
||||
public bool IsFirstInGroup { get; set; } = false;
|
||||
public string CommitSHA { get; set; } = string.Empty;
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string Time { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BlameData {
|
||||
public string File { get; set; } = string.Empty;
|
||||
public List<BlameLineInfo> LineInfos { get; set; } = new List<BlameLineInfo>();
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsBinary { get; set; } = false;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 追溯中的行信息
|
||||
/// </summary>
|
||||
public class BlameLine {
|
||||
public string LineNumber { get; set; }
|
||||
public string CommitSHA { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Time { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
}
|
22
src/Models/Bookmarks.cs
Normal file
22
src/Models/Bookmarks.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public static class Bookmarks {
|
||||
public static readonly Avalonia.Media.IBrush[] Brushes = [
|
||||
Avalonia.Media.Brushes.Transparent,
|
||||
Avalonia.Media.Brushes.Red,
|
||||
Avalonia.Media.Brushes.Orange,
|
||||
Avalonia.Media.Brushes.Gold,
|
||||
Avalonia.Media.Brushes.ForestGreen,
|
||||
Avalonia.Media.Brushes.DarkCyan,
|
||||
Avalonia.Media.Brushes.DeepSkyBlue,
|
||||
Avalonia.Media.Brushes.Purple,
|
||||
];
|
||||
|
||||
public static readonly List<int> Supported = new List<int>();
|
||||
|
||||
static Bookmarks() {
|
||||
for (int i = 0; i < Brushes.Length; i++) Supported.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 分支数据
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public class Branch {
|
||||
public string Name { get; set; }
|
||||
public string FullName { get; set; }
|
||||
|
|
152
src/Models/BranchTreeNode.cs
Normal file
152
src/Models/BranchTreeNode.cs
Normal file
|
@ -0,0 +1,152 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public enum BranchTreeNodeType {
|
||||
Remote,
|
||||
Folder,
|
||||
Branch,
|
||||
}
|
||||
|
||||
public class BranchTreeNode {
|
||||
public string Name { get; set; }
|
||||
public BranchTreeNodeType Type { get; set; }
|
||||
public object Backend { get; set; }
|
||||
public bool IsExpanded { get; set; }
|
||||
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
|
||||
|
||||
public bool IsUpstreamTrackStatusVisible {
|
||||
get => IsBranch && !string.IsNullOrEmpty((Backend as Branch).UpstreamTrackStatus);
|
||||
}
|
||||
|
||||
public string UpstreamTrackStatus {
|
||||
get => Type == BranchTreeNodeType.Branch ? (Backend as Branch).UpstreamTrackStatus : "";
|
||||
}
|
||||
|
||||
public bool IsRemote {
|
||||
get => Type == BranchTreeNodeType.Remote;
|
||||
}
|
||||
|
||||
public bool IsFolder {
|
||||
get => Type == BranchTreeNodeType.Folder;
|
||||
}
|
||||
|
||||
public bool IsBranch {
|
||||
get => Type == BranchTreeNodeType.Branch;
|
||||
}
|
||||
|
||||
public bool IsCurrent {
|
||||
get => IsBranch && (Backend as Branch).IsCurrent;
|
||||
}
|
||||
|
||||
public class Builder {
|
||||
public List<BranchTreeNode> Locals => _locals;
|
||||
public List<BranchTreeNode> Remotes => _remotes;
|
||||
|
||||
public void Run(List<Branch> branches, List<Remote> remotes) {
|
||||
foreach (var remote in remotes) {
|
||||
var path = $"remote/{remote.Name}";
|
||||
var node = new BranchTreeNode() {
|
||||
Name = remote.Name,
|
||||
Type = BranchTreeNodeType.Remote,
|
||||
Backend = remote,
|
||||
IsExpanded = _expanded.Contains(path),
|
||||
};
|
||||
|
||||
_maps.Add(path, node);
|
||||
_remotes.Add(node);
|
||||
}
|
||||
|
||||
foreach (var branch in branches) {
|
||||
if (branch.IsLocal) {
|
||||
MakeBranchNode(branch, _locals, "local");
|
||||
} else {
|
||||
var remote = _remotes.Find(x => x.Name == branch.Remote);
|
||||
if (remote != null) MakeBranchNode(branch, remote.Children, $"remote/{remote.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
SortNodes(_locals);
|
||||
SortNodes(_remotes);
|
||||
}
|
||||
|
||||
public void CollectExpandedNodes(List<BranchTreeNode> nodes, bool isLocal) {
|
||||
CollectExpandedNodes(nodes, isLocal ? "local" : "remote");
|
||||
}
|
||||
|
||||
private void CollectExpandedNodes(List<BranchTreeNode> nodes, string prefix) {
|
||||
foreach (var node in nodes) {
|
||||
var path = prefix + "/" + node.Name;
|
||||
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) _expanded.Add(path);
|
||||
CollectExpandedNodes(node.Children, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void MakeBranchNode(Branch branch, List<BranchTreeNode> roots, string prefix) {
|
||||
var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (subs.Length == 1) {
|
||||
var node = new BranchTreeNode() {
|
||||
Name = subs[0],
|
||||
Type = BranchTreeNodeType.Branch,
|
||||
Backend = branch,
|
||||
IsExpanded = false,
|
||||
};
|
||||
roots.Add(node);
|
||||
return;
|
||||
}
|
||||
|
||||
BranchTreeNode lastFolder = null;
|
||||
string path = prefix;
|
||||
for (int i = 0; i < subs.Length - 1; i++) {
|
||||
path = string.Concat(path, "/", subs[i]);
|
||||
if (_maps.ContainsKey(path)) {
|
||||
lastFolder = _maps[path];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new BranchTreeNode() {
|
||||
Name = subs[i],
|
||||
Type = BranchTreeNodeType.Folder,
|
||||
IsExpanded = _expanded.Contains(path),
|
||||
};
|
||||
roots.Add(lastFolder);
|
||||
_maps.Add(path, lastFolder);
|
||||
} else {
|
||||
var folder = new BranchTreeNode() {
|
||||
Name = subs[i],
|
||||
Type = BranchTreeNodeType.Folder,
|
||||
IsExpanded = _expanded.Contains(path),
|
||||
};
|
||||
_maps.Add(path, folder);
|
||||
lastFolder.Children.Add(folder);
|
||||
lastFolder = folder;
|
||||
}
|
||||
}
|
||||
|
||||
var last = new BranchTreeNode() {
|
||||
Name = subs[subs.Length - 1],
|
||||
Type = BranchTreeNodeType.Branch,
|
||||
Backend = branch,
|
||||
IsExpanded = false,
|
||||
};
|
||||
lastFolder.Children.Add(last);
|
||||
}
|
||||
|
||||
private void SortNodes(List<BranchTreeNode> nodes) {
|
||||
nodes.Sort((l, r) => {
|
||||
if (l.Type == r.Type) {
|
||||
return l.Name.CompareTo(r.Name);
|
||||
} else {
|
||||
return (int)(l.Type) - (int)(r.Type);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var node in nodes) SortNodes(node.Children);
|
||||
}
|
||||
|
||||
private List<BranchTreeNode> _locals = new List<BranchTreeNode>();
|
||||
private List<BranchTreeNode> _remotes = new List<BranchTreeNode>();
|
||||
private HashSet<string> _expanded = new HashSet<string>();
|
||||
private Dictionary<string, BranchTreeNode> _maps = new Dictionary<string, BranchTreeNode>();
|
||||
}
|
||||
}
|
||||
}
|
21
src/Models/CRLFMode.cs
Normal file
21
src/Models/CRLFMode.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class CRLFMode {
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Desc { get; set; }
|
||||
|
||||
public static List<CRLFMode> Supported = new List<CRLFMode>() {
|
||||
new CRLFMode("TRUE", "true", "Commit as LF, checkout as CRLF"),
|
||||
new CRLFMode("INPUT", "input", "Only convert for commit"),
|
||||
new CRLFMode("FALSE", "false", "Do NOT convert"),
|
||||
};
|
||||
|
||||
public CRLFMode(string name, string value, string desc) {
|
||||
Name = name;
|
||||
Value = value;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 自动换行处理方式
|
||||
/// </summary>
|
||||
public class CRLFOption {
|
||||
public string Display { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Desc { get; set; }
|
||||
|
||||
public static List<CRLFOption> Supported = new List<CRLFOption>() {
|
||||
new CRLFOption("TRUE", "true", "Commit as LF, checkout as CRLF"),
|
||||
new CRLFOption("INPUT", "input", "Only convert for commit"),
|
||||
new CRLFOption("FALSE", "false", "Do NOT convert"),
|
||||
};
|
||||
|
||||
public CRLFOption(string display, string value, string desc) {
|
||||
Display = display;
|
||||
Value = value;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +1,41 @@
|
|||
namespace SourceGit.Models {
|
||||
namespace SourceGit.Models {
|
||||
public enum ChangeViewMode {
|
||||
List,
|
||||
Grid,
|
||||
Tree,
|
||||
}
|
||||
|
||||
public enum ChangeState {
|
||||
None,
|
||||
Modified,
|
||||
Added,
|
||||
Deleted,
|
||||
Renamed,
|
||||
Copied,
|
||||
Unmerged,
|
||||
Untracked
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Git变更
|
||||
/// </summary>
|
||||
public class Change {
|
||||
|
||||
/// <summary>
|
||||
/// 显示模式
|
||||
/// </summary>
|
||||
public enum DisplayMode {
|
||||
Tree,
|
||||
List,
|
||||
Grid,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更状态码
|
||||
/// </summary>
|
||||
public enum Status {
|
||||
None,
|
||||
Modified,
|
||||
Added,
|
||||
Deleted,
|
||||
Renamed,
|
||||
Copied,
|
||||
Unmerged,
|
||||
Untracked,
|
||||
}
|
||||
|
||||
public Status Index { get; set; }
|
||||
public Status WorkTree { get; set; } = Status.None;
|
||||
public ChangeState Index { get; set; }
|
||||
public ChangeState WorkTree { get; set; } = ChangeState.None;
|
||||
public string Path { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
|
||||
public bool IsConflit {
|
||||
get {
|
||||
if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true;
|
||||
if (Index == Status.Added && WorkTree == Status.Added) return true;
|
||||
if (Index == Status.Deleted && WorkTree == Status.Deleted) return true;
|
||||
if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged) return true;
|
||||
if (Index == ChangeState.Added && WorkTree == ChangeState.Added) return true;
|
||||
if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Set(Status index, Status workTree = Status.None) {
|
||||
public void Set(ChangeState index, ChangeState workTree = ChangeState.None) {
|
||||
Index = index;
|
||||
WorkTree = workTree;
|
||||
|
||||
if (index == Status.Renamed || workTree == Status.Renamed) {
|
||||
if (index == ChangeState.Renamed || workTree == ChangeState.Renamed) {
|
||||
var idx = Path.IndexOf('\t');
|
||||
if (idx >= 0) {
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
using Avalonia;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 提交记录
|
||||
/// </summary>
|
||||
public class Commit {
|
||||
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
public string SHA { get; set; } = string.Empty;
|
||||
public string ShortSHA => SHA.Substring(0, 8);
|
||||
public User Author { get; set; } = User.Invalid;
|
||||
public ulong AuthorTime { get; set; } = 0;
|
||||
public User Committer { get; set; } = User.Invalid;
|
||||
|
@ -23,10 +17,18 @@ namespace SourceGit.Models {
|
|||
public bool IsMerged { get; set; } = false;
|
||||
public Thickness Margin { get; set; } = new Thickness(0);
|
||||
|
||||
public string AuthorTimeStr => UTC_START.AddSeconds(AuthorTime).ToString("yyyy-MM-dd HH:mm:ss");
|
||||
public string CommitterTimeStr => UTC_START.AddSeconds(CommitterTime).ToString("yyyy-MM-dd HH:mm:ss");
|
||||
public string AuthorTimeShortStr => UTC_START.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
|
||||
public string CommitterTimeShortStr => UTC_START.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
|
||||
public string AuthorTimeStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string CommitterTimeStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd HH:mm:ss");
|
||||
public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
|
||||
public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
|
||||
|
||||
public bool IsCommitterVisible {
|
||||
get => Author != Committer || AuthorTime != CommitterTime;
|
||||
}
|
||||
|
||||
public string FullMessage {
|
||||
get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}";
|
||||
}
|
||||
|
||||
public static void ParseUserAndTime(string data, ref User user, ref ulong time) {
|
||||
var userEndIdx = data.IndexOf('>');
|
||||
|
@ -36,5 +38,7 @@ namespace SourceGit.Models {
|
|||
user = User.FindOrAdd(data.Substring(0, userEndIdx));
|
||||
time = timeEndIdx < 0 ? 0 : ulong.Parse(data.Substring(userEndIdx + 2, timeEndIdx - userEndIdx - 2));
|
||||
}
|
||||
|
||||
private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
}
|
||||
}
|
||||
|
|
205
src/Models/CommitGraph.cs
Normal file
205
src/Models/CommitGraph.cs
Normal file
|
@ -0,0 +1,205 @@
|
|||
using Avalonia;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class CommitGraph {
|
||||
public class Path {
|
||||
public List<Point> Points = new List<Point>();
|
||||
public int Color = 0;
|
||||
}
|
||||
|
||||
public class PathHelper {
|
||||
public string Next;
|
||||
public bool IsMerged;
|
||||
public double LastX;
|
||||
public double LastY;
|
||||
public double EndY;
|
||||
public Path Path;
|
||||
|
||||
public PathHelper(string next, bool isMerged, int color, Point start) {
|
||||
Next = next;
|
||||
IsMerged = isMerged;
|
||||
LastX = start.X;
|
||||
LastY = start.Y;
|
||||
EndY = LastY;
|
||||
|
||||
Path = new Path();
|
||||
Path.Color = color;
|
||||
Path.Points.Add(start);
|
||||
}
|
||||
|
||||
public PathHelper(string next, bool isMerged, int color, Point start, Point to) {
|
||||
Next = next;
|
||||
IsMerged = isMerged;
|
||||
LastX = to.X;
|
||||
LastY = to.Y;
|
||||
EndY = LastY;
|
||||
|
||||
Path = new Path();
|
||||
Path.Color = color;
|
||||
Path.Points.Add(start);
|
||||
Path.Points.Add(to);
|
||||
}
|
||||
|
||||
public void Add(double x, double y, double halfHeight, bool isEnd = false) {
|
||||
if (x > LastX) {
|
||||
Add(new Point(LastX, LastY));
|
||||
Add(new Point(x, y - halfHeight));
|
||||
if (isEnd) Add(new Point(x, y));
|
||||
} else if (x < LastX) {
|
||||
if (y > LastY + halfHeight) Add(new Point(LastX, LastY + halfHeight));
|
||||
Add(new Point(x, y));
|
||||
} else if (isEnd) {
|
||||
Add(new Point(x, y));
|
||||
}
|
||||
|
||||
LastX = x;
|
||||
LastY = y;
|
||||
}
|
||||
|
||||
private void Add(Point p) {
|
||||
if (EndY < p.Y) {
|
||||
Path.Points.Add(p);
|
||||
EndY = p.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Link {
|
||||
public Point Start;
|
||||
public Point Control;
|
||||
public Point End;
|
||||
public int Color;
|
||||
}
|
||||
|
||||
public class Dot {
|
||||
public Point Center;
|
||||
public int Color;
|
||||
}
|
||||
|
||||
public List<Path> Paths { get; set; } = new List<Path>();
|
||||
public List<Link> Links { get; set; } = new List<Link>();
|
||||
public List<Dot> Dots { get; set; } = new List<Dot>();
|
||||
|
||||
public static CommitGraph Parse(List<Commit> commits, double rowHeight, int colorCount) {
|
||||
double UNIT_WIDTH = 12;
|
||||
double HALF_WIDTH = 6;
|
||||
double UNIT_HEIGHT = rowHeight;
|
||||
double HALF_HEIGHT = rowHeight / 2;
|
||||
|
||||
var temp = new CommitGraph();
|
||||
var unsolved = new List<PathHelper>();
|
||||
var mapUnsolved = new Dictionary<string, PathHelper>();
|
||||
var ended = new List<PathHelper>();
|
||||
var offsetY = -HALF_HEIGHT;
|
||||
var colorIdx = 0;
|
||||
|
||||
foreach (var commit in commits) {
|
||||
var major = null as PathHelper;
|
||||
var isMerged = commit.IsMerged;
|
||||
var oldCount = unsolved.Count;
|
||||
|
||||
// Update current y offset
|
||||
offsetY += UNIT_HEIGHT;
|
||||
|
||||
// Find first curves that links to this commit and marks others that links to this commit ended.
|
||||
double offsetX = -HALF_WIDTH;
|
||||
foreach (var l in unsolved) {
|
||||
if (l.Next == commit.SHA) {
|
||||
if (major == null) {
|
||||
offsetX += UNIT_WIDTH;
|
||||
major = l;
|
||||
|
||||
if (commit.Parents.Count > 0) {
|
||||
major.Next = commit.Parents[0];
|
||||
if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major);
|
||||
} else {
|
||||
major.Next = "ENDED";
|
||||
ended.Add(l);
|
||||
}
|
||||
|
||||
major.Add(offsetX, offsetY, HALF_HEIGHT);
|
||||
} else {
|
||||
ended.Add(l);
|
||||
}
|
||||
|
||||
isMerged = isMerged || l.IsMerged;
|
||||
} else {
|
||||
if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l);
|
||||
offsetX += UNIT_WIDTH;
|
||||
l.Add(offsetX, offsetY, HALF_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new curve for branch head
|
||||
if (major == null && commit.Parents.Count > 0) {
|
||||
offsetX += UNIT_WIDTH;
|
||||
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
|
||||
unsolved.Add(major);
|
||||
temp.Paths.Add(major.Path);
|
||||
colorIdx = (colorIdx + 1) % colorCount;
|
||||
}
|
||||
|
||||
// Calculate link position of this commit.
|
||||
Point position = new Point(offsetX, offsetY);
|
||||
if (major != null) {
|
||||
major.IsMerged = isMerged;
|
||||
position = new Point(major.LastX, offsetY);
|
||||
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color });
|
||||
} else {
|
||||
temp.Dots.Add(new Dot() { Center = position, Color = 0 });
|
||||
}
|
||||
|
||||
// Deal with parents
|
||||
for (int j = 1; j < commit.Parents.Count; j++) {
|
||||
var parent = commit.Parents[j];
|
||||
if (mapUnsolved.ContainsKey(parent)) {
|
||||
var l = mapUnsolved[parent];
|
||||
var link = new Link();
|
||||
|
||||
link.Start = position;
|
||||
link.End = new Point(l.LastX, offsetY + HALF_HEIGHT);
|
||||
link.Control = new Point(link.End.X, link.Start.Y);
|
||||
link.Color = l.Path.Color;
|
||||
temp.Links.Add(link);
|
||||
} else {
|
||||
offsetX += UNIT_WIDTH;
|
||||
|
||||
// Create new curve for parent commit that not includes before
|
||||
var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT));
|
||||
unsolved.Add(l);
|
||||
temp.Paths.Add(l.Path);
|
||||
colorIdx = (colorIdx + 1) % colorCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ended curves from unsolved
|
||||
foreach (var l in ended) {
|
||||
l.Add(position.X, position.Y, HALF_HEIGHT, true);
|
||||
unsolved.Remove(l);
|
||||
}
|
||||
|
||||
// Margins & merge state (used by datagrid).
|
||||
commit.IsMerged = isMerged;
|
||||
commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH), 0, 0, 0);
|
||||
|
||||
// Clean up
|
||||
ended.Clear();
|
||||
mapUnsolved.Clear();
|
||||
}
|
||||
|
||||
// Deal with curves haven't ended yet.
|
||||
for (int i = 0; i < unsolved.Count; i++) {
|
||||
var path = unsolved[i];
|
||||
var endY = (commits.Count - 0.5) * UNIT_HEIGHT;
|
||||
|
||||
if (path.Path.Points.Count == 1 && path.Path.Points[0].Y == endY) continue;
|
||||
path.Add((i + 0.5) * UNIT_WIDTH, endY + HALF_HEIGHT, HALF_HEIGHT, true);
|
||||
}
|
||||
unsolved.Clear();
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 崩溃日志生成
|
||||
/// </summary>
|
||||
public class CrashInfo {
|
||||
public static void Create(Exception e) {
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("Crash: ");
|
||||
builder.Append(e.Message);
|
||||
builder.Append("\n\n");
|
||||
builder.Append("----------------------------\n");
|
||||
builder.Append($"Windows OS: {Environment.OSVersion}\n");
|
||||
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
|
||||
builder.Append($"Platform: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
|
||||
builder.Append($"Source: {e.Source}\n");
|
||||
builder.Append($"---------------------------\n\n");
|
||||
builder.Append(e.StackTrace);
|
||||
|
||||
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||
var file = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
|
||||
file = Path.Combine(file, $"sourcegit_crash_{time}.log");
|
||||
File.WriteAllText(file, builder.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
namespace SourceGit.Models {
|
||||
using Avalonia.Media;
|
||||
|
||||
/// <summary>
|
||||
/// 修饰类型
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public enum DecoratorType {
|
||||
None,
|
||||
CurrentBranchHead,
|
||||
|
@ -11,11 +9,15 @@ namespace SourceGit.Models {
|
|||
Tag,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交的附加修饰
|
||||
/// </summary>
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
95
src/Models/DiffOption.cs
Normal file
95
src/Models/DiffOption.cs
Normal file
|
@ -0,0 +1,95 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class DiffOption {
|
||||
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) {
|
||||
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.
|
||||
/// </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 string _orgPath = string.Empty;
|
||||
private string _path = string.Empty;
|
||||
private string _extra = string.Empty;
|
||||
private List<string> _revisions = new List<string>();
|
||||
}
|
||||
}
|
56
src/Models/DiffResult.cs
Normal file
56
src/Models/DiffResult.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
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 string OldLine { get; set; } = "";
|
||||
public string NewLine { get; set; } = "";
|
||||
public List<TextInlineRange> Highlights { get; set; } = new List<TextInlineRange>();
|
||||
|
||||
public TextDiffLine() { }
|
||||
public TextDiffLine(TextDiffLineType type, string content, string oldLine, string newLine) {
|
||||
Type = type;
|
||||
Content = content;
|
||||
OldLine = oldLine;
|
||||
NewLine = newLine;
|
||||
}
|
||||
}
|
||||
|
||||
public class TextDiff {
|
||||
public string File { get; set; } = string.Empty;
|
||||
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
|
||||
public int MaxLineNumber = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 用于在PATH中检测可执行文件
|
||||
/// </summary>
|
||||
public class ExecutableFinder {
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/shlwapi/nf-shlwapi-pathfindonpathw
|
||||
// https://www.pinvoke.net/default.aspx/shlwapi.PathFindOnPath
|
||||
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
|
||||
|
||||
/// <summary>
|
||||
/// 从PATH中找到可执行文件路径
|
||||
/// </summary>
|
||||
/// <param name="exec"></param>
|
||||
/// <returns></returns>
|
||||
public static string Find(string exec) {
|
||||
var builder = new StringBuilder(exec, 259);
|
||||
var rs = PathFindOnPath(builder, null);
|
||||
return rs ? builder.ToString() : null;
|
||||
}
|
||||
}
|
||||
}
|
36
src/Models/ExternalMergeTools.cs
Normal file
36
src/Models/ExternalMergeTools.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
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\""),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalMergeTools(int type, string name, string exec, string cmd, string diffCmd) {
|
||||
Type = type;
|
||||
Name = name;
|
||||
Exec = exec;
|
||||
Cmd = cmd;
|
||||
DiffCmd = diffCmd;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 文件大小变化
|
||||
/// </summary>
|
||||
public class FileSizeChange {
|
||||
public long OldSize = 0;
|
||||
public long NewSize = 0;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// GitFlow的分支类型
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public enum GitFlowBranchType {
|
||||
None,
|
||||
Feature,
|
||||
|
@ -9,9 +6,6 @@ namespace SourceGit.Models {
|
|||
Hotfix,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GitFlow相关设置
|
||||
/// </summary>
|
||||
public class GitFlow {
|
||||
public string Feature { get; set; }
|
||||
public string Release { get; set; }
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
public class InstalledFont {
|
||||
public string Name { get; set; }
|
||||
public int FamilyIndex { get; set; }
|
||||
|
||||
public static List<InstalledFont> GetFonts {
|
||||
get {
|
||||
var fontList = new List<InstalledFont>();
|
||||
|
||||
var fontCollection = Fonts.SystemFontFamilies;
|
||||
var familyCount = fontCollection.Count;
|
||||
|
||||
for (int i = 0; i < familyCount; i++) {
|
||||
var fontFamily = fontCollection.ElementAt(i);
|
||||
var familyNames = fontFamily.FamilyNames;
|
||||
|
||||
if (!familyNames.TryGetValue(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.Name), out var name)) {
|
||||
if (!familyNames.TryGetValue(XmlLanguage.GetLanguage("en-us"), out name)) {
|
||||
name = familyNames.FirstOrDefault().Value;
|
||||
}
|
||||
}
|
||||
|
||||
fontList.Add(new InstalledFont() {
|
||||
Name = name,
|
||||
FamilyIndex = i
|
||||
});
|
||||
}
|
||||
|
||||
fontList.Sort((p, n) => string.Compare(p.Name, n.Name, StringComparison.Ordinal));
|
||||
|
||||
return fontList;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// LFS对象变更
|
||||
/// </summary>
|
||||
public class LFSChange {
|
||||
public LFSObject Old;
|
||||
public LFSObject New;
|
||||
public bool IsValid => Old != null || New != null;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// LFS对象
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public class LFSObject {
|
||||
public string OID { get; set; }
|
||||
public long Size { get; set; }
|
||||
public string Oid { get; set; } = string.Empty;
|
||||
public long Size { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 支持的语言
|
||||
/// </summary>
|
||||
public class Locale {
|
||||
public string Name { get; set; }
|
||||
public string Resource { get; set; }
|
||||
|
||||
public static List<Locale> Supported = new List<Locale>() {
|
||||
new Locale("English", "en_US"),
|
||||
new Locale("简体中文", "zh_CN"),
|
||||
};
|
||||
|
||||
public Locale(string name, string res) {
|
||||
Name = name;
|
||||
Resource = res;
|
||||
}
|
||||
|
||||
public static void Change() {
|
||||
var lang = Preference.Instance.General.Locale;
|
||||
foreach (var rs in App.Current.Resources.MergedDictionaries) {
|
||||
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Locales/", StringComparison.Ordinal)) {
|
||||
rs.Source = new Uri($"pack://application:,,,/Resources/Locales/{lang}.xaml", UriKind.Absolute);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/Models/Locales.cs
Normal file
18
src/Models/Locales.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 合并方式
|
||||
/// </summary>
|
||||
public class MergeOption {
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
|
||||
public static List<MergeOption> Supported = new List<MergeOption>() {
|
||||
new MergeOption("Default", "Fast-forward if possible", ""),
|
||||
new MergeOption("No Fast-forward", "Always create a merge commit", "--no-ff"),
|
||||
new MergeOption("Squash", "Use '--squash'", "--squash"),
|
||||
new MergeOption("Don't commit", "Merge without commit", "--no-commit"),
|
||||
};
|
||||
|
||||
public MergeOption(string n, string d, string a) {
|
||||
Name = n;
|
||||
Desc = d;
|
||||
Arg = a;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 外部合并工具
|
||||
/// </summary>
|
||||
public class MergeTool {
|
||||
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 Func<string> Finder { get; set; }
|
||||
|
||||
public static List<MergeTool> Supported = new List<MergeTool>() {
|
||||
new MergeTool(0, "--", "", "", "", () => ""),
|
||||
new MergeTool(1, "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\"", FindVSCode),
|
||||
new MergeTool(2, "Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" /m", "\"$LOCAL\" \"$REMOTE\"", FindVSMerge),
|
||||
new MergeTool(3, "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\"", FindTortoiseMerge),
|
||||
new MergeTool(4, "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\"", FindKDiff3),
|
||||
new MergeTool(5, "Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\"", FindBCompare),
|
||||
new MergeTool(6, "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\"", FindWinMerge),
|
||||
};
|
||||
|
||||
public MergeTool(int type, string name, string exec, string cmd, string diffCmd, Func<string> finder) {
|
||||
Type = type;
|
||||
Name = name;
|
||||
Exec = exec;
|
||||
Cmd = cmd;
|
||||
DiffCmd = diffCmd;
|
||||
Finder = finder;
|
||||
}
|
||||
|
||||
private static string FindVSCode() {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1");
|
||||
if (vscode != null) {
|
||||
return vscode.GetValue("DisplayIcon") as string;
|
||||
}
|
||||
|
||||
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
|
||||
if (vscode != null) {
|
||||
return vscode.GetValue("DisplayIcon") as string;
|
||||
}
|
||||
|
||||
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1");
|
||||
if (vscode != null) {
|
||||
return vscode.GetValue("DisplayIcon") as string;
|
||||
}
|
||||
|
||||
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
|
||||
if (vscode != null) {
|
||||
return vscode.GetValue("DisplayIcon") as string;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static string FindVSMerge() {
|
||||
var dir = @"C:\Program Files (x86)\Microsoft Visual Studio";
|
||||
if (Directory.Exists($"{dir}\\2019")) {
|
||||
dir += "\\2019";
|
||||
} else if (Directory.Exists($"{dir}\\2017")) {
|
||||
dir += "\\2017";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Directory.Exists($"{dir}\\Community")) {
|
||||
dir += "\\Community";
|
||||
} else if (Directory.Exists($"{dir}\\Enterprise")) {
|
||||
dir += "\\Enterprise";
|
||||
} else if (Directory.Exists($"{dir}\\Professional")) {
|
||||
dir += "\\Professional";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe";
|
||||
}
|
||||
|
||||
private static string FindTortoiseMerge() {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var tortoise = root.OpenSubKey("SOFTWARE\\TortoiseGit") ?? root.OpenSubKey("SOFTWARE\\TortoiseSVN");
|
||||
if (tortoise == null) return "";
|
||||
return tortoise.GetValue("TMergePath") as string;
|
||||
}
|
||||
|
||||
private static string FindKDiff3() {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext");
|
||||
if (kdiff == null) return "";
|
||||
return kdiff.GetValue("diffcommand") as string;
|
||||
}
|
||||
|
||||
private static string FindBCompare() {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare");
|
||||
if (bc == null) return "";
|
||||
|
||||
var exec = bc.GetValue("ExePath") as string;
|
||||
var dir = Path.GetDirectoryName(exec);
|
||||
return $"{dir}\\BComp.exe";
|
||||
}
|
||||
|
||||
private static string FindWinMerge() {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.CurrentUser,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var merge = root.OpenSubKey(@"SOFTWARE\Thingamahoochie\WinMerge");
|
||||
if (merge == null) return "";
|
||||
|
||||
var exec = merge.GetValue("Executable") as string;
|
||||
return exec;
|
||||
}
|
||||
}
|
||||
}
|
10
src/Models/Notification.cs
Normal file
10
src/Models/Notification.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 提交中元素类型
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public enum ObjectType {
|
||||
None,
|
||||
Blob,
|
||||
|
@ -10,9 +7,6 @@ namespace SourceGit.Models {
|
|||
Commit,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Git提交中的元素
|
||||
/// </summary>
|
||||
public class Object {
|
||||
public string SHA { get; set; }
|
||||
public ObjectType Type { get; set; }
|
||||
|
|
|
@ -1,297 +0,0 @@
|
|||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 程序配置
|
||||
/// </summary>
|
||||
public class Preference {
|
||||
private static readonly string SAVE_PATH = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"SourceGit",
|
||||
"preference_v4.json");
|
||||
private static Preference instance = null;
|
||||
|
||||
/// <summary>
|
||||
/// 起始页仓库列表排序方式
|
||||
/// </summary>
|
||||
public enum SortMethod {
|
||||
ByName,
|
||||
ByRecentlyOpened,
|
||||
ByBookmark,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用配置
|
||||
/// </summary>
|
||||
public class GeneralInfo {
|
||||
|
||||
/// <summary>
|
||||
/// 显示语言
|
||||
/// </summary>
|
||||
public string Locale { get; set; } = "en_US";
|
||||
|
||||
/// <summary>
|
||||
/// 系统字体
|
||||
/// </summary>
|
||||
public string FontFamilyWindowSetting { get; set; } = "Microsoft YaHei UI";
|
||||
|
||||
[JsonIgnore]
|
||||
public string FontFamilyWindow {
|
||||
get => FontFamilyWindowSetting + ",Microsoft YaHei UI";
|
||||
set => FontFamilyWindowSetting = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户字体(提交列表、提交日志、差异比较等)
|
||||
/// </summary>
|
||||
public string FontFamilyContentSetting { get; set; } = "Consolas";
|
||||
|
||||
[JsonIgnore]
|
||||
public string FontFamilyContent {
|
||||
get => FontFamilyContentSetting + ",Microsoft YaHei UI";
|
||||
set => FontFamilyContentSetting = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用深色主题
|
||||
/// </summary>
|
||||
public bool UseDarkTheme { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 历史提交记录最多显示的条目数
|
||||
/// </summary>
|
||||
public uint MaxHistoryCommits { get; set; } = 20000;
|
||||
|
||||
/// <summary>
|
||||
/// 起始页仓库列表排序规则
|
||||
/// </summary>
|
||||
public SortMethod SortBy { get; set; } = SortMethod.ByName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Git配置
|
||||
/// </summary>
|
||||
public class GitInfo {
|
||||
|
||||
/// <summary>
|
||||
/// git.exe所在路径
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认克隆路径
|
||||
/// </summary>
|
||||
public string DefaultCloneDir { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启用自动拉取远程变更(每10分钟一次)
|
||||
/// </summary>
|
||||
public bool AutoFetchRemotes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 在本地变更列表中显示未跟踪文件
|
||||
/// </summary>
|
||||
public bool IncludeUntrackedInWC { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 外部合并工具配置
|
||||
/// </summary>
|
||||
public class MergeToolInfo {
|
||||
/// <summary>
|
||||
/// 合并工具类型
|
||||
/// </summary>
|
||||
public int Type { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 合并工具可执行文件路径
|
||||
/// </summary>
|
||||
public string Path { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用设置
|
||||
/// </summary>
|
||||
public class WindowInfo {
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次设置的宽度
|
||||
/// </summary>
|
||||
public double Width { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次设置的高度
|
||||
/// </summary>
|
||||
public double Height { get; set; } = 600;
|
||||
|
||||
/// <summary>
|
||||
/// 保存上次关闭时是否最大化中
|
||||
/// </summary>
|
||||
public WindowState State { get; set; } = WindowState.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 将提交信息面板与提交记录左右排布
|
||||
/// </summary>
|
||||
public bool MoveCommitInfoRight { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 使用合并Diff视图
|
||||
/// </summary>
|
||||
public bool UseCombinedDiff { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Pull时是否使用Rebase替换Merge
|
||||
/// </summary>
|
||||
public bool UseRebaseOnPull { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Pull时是否使用自动暂存
|
||||
/// </summary>
|
||||
public bool UseAutoStashOnPull { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 未暂存视图中变更显示方式
|
||||
/// </summary>
|
||||
public Change.DisplayMode ChangeInUnstaged { get; set; } = Change.DisplayMode.Tree;
|
||||
|
||||
/// <summary>
|
||||
/// 暂存视图中变更显示方式
|
||||
/// </summary>
|
||||
public Change.DisplayMode ChangeInStaged { get; set; } = Change.DisplayMode.Tree;
|
||||
|
||||
/// <summary>
|
||||
/// 提交信息视图中变更显示方式
|
||||
/// </summary>
|
||||
public Change.DisplayMode ChangeInCommitInfo { get; set; } = Change.DisplayMode.Tree;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复上次打开的窗口
|
||||
/// </summary>
|
||||
public class RestoreTabs {
|
||||
|
||||
/// <summary>
|
||||
/// 是否开启该功能
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 上次打开的仓库
|
||||
/// </summary>
|
||||
public List<string> Opened { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 最后浏览的仓库
|
||||
/// </summary>
|
||||
public string Actived { get; set; } = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局配置
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public static Preference Instance {
|
||||
get {
|
||||
if (instance == null) return Load();
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测配置是否正常
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsReady {
|
||||
get => File.Exists(Git.Path) && new Commands.Version().Query() != null;
|
||||
}
|
||||
|
||||
#region DATA
|
||||
public GeneralInfo General { get; set; } = new GeneralInfo();
|
||||
public GitInfo Git { get; set; } = new GitInfo();
|
||||
public MergeToolInfo MergeTool { get; set; } = new MergeToolInfo();
|
||||
public WindowInfo Window { get; set; } = new WindowInfo();
|
||||
public List<Repository> Repositories { get; set; } = new List<Repository>();
|
||||
public RestoreTabs Restore { get; set; } = new RestoreTabs();
|
||||
#endregion
|
||||
|
||||
#region LOAD_SAVE
|
||||
public static Preference Load() {
|
||||
if (!File.Exists(SAVE_PATH)) {
|
||||
instance = new Preference();
|
||||
} else {
|
||||
try {
|
||||
instance = JsonSerializer.Deserialize<Preference>(File.ReadAllText(SAVE_PATH));
|
||||
} catch {
|
||||
instance = new Preference();
|
||||
}
|
||||
}
|
||||
|
||||
if (!instance.IsReady) {
|
||||
var reg = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
var git = reg.OpenSubKey("SOFTWARE\\GitForWindows");
|
||||
if (git != null) {
|
||||
instance.Git.Path = Path.Combine(git.GetValue("InstallPath") as string, "bin", "git.exe");
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void Save() {
|
||||
var dir = Path.GetDirectoryName(SAVE_PATH);
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
var data = JsonSerializer.Serialize(instance, new JsonSerializerOptions() { WriteIndented = true });
|
||||
File.WriteAllText(SAVE_PATH, data);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region METHOD_ON_REPOSITORIES
|
||||
public Repository AddRepository(string path, string gitDir) {
|
||||
var repo = FindRepository(path);
|
||||
if (repo != null) return repo;
|
||||
|
||||
var dir = new DirectoryInfo(path);
|
||||
repo = new Repository() {
|
||||
Path = dir.FullName,
|
||||
GitDir = gitDir,
|
||||
Name = dir.Name
|
||||
};
|
||||
|
||||
Repositories.Add(repo);
|
||||
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
return repo;
|
||||
}
|
||||
|
||||
public Repository FindRepository(string path) {
|
||||
var dir = new DirectoryInfo(path);
|
||||
foreach (var repo in Repositories) {
|
||||
if (repo.Path == dir.FullName) return repo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveRepository(string path) {
|
||||
var dir = new DirectoryInfo(path);
|
||||
var removedIdx = -1;
|
||||
|
||||
for (int i = 0; i < Repositories.Count; i++) {
|
||||
if (Repositories[i].Path == dir.FullName) {
|
||||
removedIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedIdx >= 0) Repositories.RemoveAt(removedIdx);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -1,10 +1,31 @@
|
|||
namespace SourceGit.Models {
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// 远程
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public class Remote {
|
||||
private static readonly Regex[] URL_FORMATS = [
|
||||
new Regex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$"),
|
||||
new Regex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-\.]+\.git$"),
|
||||
new Regex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$"),
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 用于更新过滤器的参数
|
||||
/// </summary>
|
||||
public class FilterUpdateParam {
|
||||
/// <summary>
|
||||
/// 是否是添加过滤的操作,false代表删除
|
||||
/// </summary>
|
||||
public bool IsAdd = false;
|
||||
|
||||
/// <summary>
|
||||
/// 过滤内容
|
||||
/// </summary>
|
||||
public string Name = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仓库
|
||||
/// </summary>
|
||||
public class Repository {
|
||||
|
||||
#region PROPERTIES_SAVED
|
||||
public string Name {
|
||||
get => name;
|
||||
set {
|
||||
if (name != value) {
|
||||
name = value;
|
||||
Watcher.NotifyDisplayNameChanged(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Path { get; set; } = "";
|
||||
public string GitDir { get; set; } = "";
|
||||
public long LastOpenTime { get; set; } = 0;
|
||||
public List<SubTree> SubTrees { get; set; } = new List<SubTree>();
|
||||
public List<string> Filters { get; set; } = new List<string>();
|
||||
public List<string> CommitMessages { get; set; } = new List<string>();
|
||||
|
||||
public int Bookmark {
|
||||
get { return bookmark; }
|
||||
set {
|
||||
if (value != bookmark) {
|
||||
bookmark = value;
|
||||
Watcher.NotifyBookmarkChanged(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PROPERTIES_RUNTIME
|
||||
[JsonIgnore] public List<Remote> Remotes = new List<Remote>();
|
||||
[JsonIgnore] public List<Branch> Branches = new List<Branch>();
|
||||
[JsonIgnore] public GitFlow GitFlow = new GitFlow();
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 记录历史输入的提交信息
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public void PushCommitMessage(string message) {
|
||||
if (string.IsNullOrEmpty(message)) return;
|
||||
|
||||
int exists = CommitMessages.Count;
|
||||
if (exists > 0) {
|
||||
var last = CommitMessages[0];
|
||||
if (last == message) return;
|
||||
}
|
||||
|
||||
if (exists >= 10) {
|
||||
CommitMessages.RemoveRange(9, exists - 9);
|
||||
}
|
||||
|
||||
CommitMessages.Insert(0, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断一个文件是否在GitDir中
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
public bool ExistsInGitDir(string file) {
|
||||
if (string.IsNullOrEmpty(file)) return false;
|
||||
string fullpath = System.IO.Path.Combine(GitDir, file);
|
||||
return Directory.Exists(fullpath) || File.Exists(fullpath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新提交记录过滤器
|
||||
/// </summary>
|
||||
/// <param name="param">更新参数</param>
|
||||
/// <returns>是否发生了变化</returns>
|
||||
public bool UpdateFilters(FilterUpdateParam param = null) {
|
||||
lock (updateFilterLock) {
|
||||
bool changed = false;
|
||||
|
||||
// 填写了参数就仅增删
|
||||
if (param != null) {
|
||||
if (param.IsAdd) {
|
||||
if (!Filters.Contains(param.Name)) {
|
||||
Filters.Add(param.Name);
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
if (Filters.Contains(param.Name)) {
|
||||
Filters.Remove(param.Name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// 未填写参数就检测,去掉无效的过滤
|
||||
if (Filters.Count > 0) {
|
||||
var invalidFilters = new List<string>();
|
||||
var branches = new Commands.Branches(Path).Result();
|
||||
var tags = new Commands.Tags(Path).Result();
|
||||
|
||||
foreach (var filter in Filters) {
|
||||
if (filter.StartsWith("refs/")) {
|
||||
if (branches.FindIndex(b => b.FullName == filter) < 0) invalidFilters.Add(filter);
|
||||
} else {
|
||||
if (tags.FindIndex(t => t.Name == filter) < 0) invalidFilters.Add(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidFilters.Count > 0) {
|
||||
foreach (var filter in invalidFilters) Filters.Remove(filter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object updateFilterLock = new object();
|
||||
private string name = string.Empty;
|
||||
private int bookmark = 0;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 重置方式
|
||||
/// </summary>
|
||||
public class ResetMode {
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
public Brush Color { get; set; }
|
||||
|
||||
public static List<ResetMode> Supported = new List<ResetMode>() {
|
||||
new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", Brushes.Green),
|
||||
new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", Brushes.Orange),
|
||||
new ResetMode("Hard", "Discard all changes", "--hard", Brushes.Red),
|
||||
};
|
||||
|
||||
public ResetMode(string n, string d, string a, Brush b) {
|
||||
Name = n;
|
||||
Desc = d;
|
||||
Arg = a;
|
||||
Color = b;
|
||||
}
|
||||
}
|
||||
}
|
17
src/Models/RevisionFile.cs
Normal file
17
src/Models/RevisionFile.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace SourceGit.Models {
|
||||
public class RevisionBinaryFile {
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 贮藏
|
||||
/// </summary>
|
||||
public class Stash {
|
||||
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
|
@ -13,6 +10,6 @@ namespace SourceGit.Models {
|
|||
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");
|
||||
public string TimeStr => UTC_START.AddSeconds(Time).ToString("yyyy/MM/dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 统计图表样品
|
||||
/// </summary>
|
||||
public class StatisticSample {
|
||||
/// <summary>
|
||||
/// 在图表中的顺序
|
||||
/// </summary>
|
||||
public int Index { get; set; }
|
||||
/// <summary>
|
||||
/// 样品名
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// 提交个数
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 子树
|
||||
/// </summary>
|
||||
public class SubTree {
|
||||
public string Prefix { get; set; }
|
||||
public string Remote { get; set; }
|
||||
public string Branch { get; set; } = "master";
|
||||
}
|
||||
}
|
|
@ -1,10 +1,6 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 标签
|
||||
/// </summary>
|
||||
namespace SourceGit.Models {
|
||||
public class Tag {
|
||||
public string Name { get; set; }
|
||||
public string SHA { get; set; }
|
||||
public bool IsFiltered { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// Diff文本文件变化
|
||||
/// </summary>
|
||||
public class TextChanges {
|
||||
|
||||
public enum LineMode {
|
||||
None,
|
||||
Normal,
|
||||
Indicator,
|
||||
Added,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
public class HighlightRange {
|
||||
public int Start { get; set; }
|
||||
public int Count { get; set; }
|
||||
public HighlightRange(int p, int n) { Start = p; Count = n; }
|
||||
}
|
||||
|
||||
public class Line {
|
||||
public int Index { get; set; } = 0;
|
||||
public LineMode Mode { get; set; } = LineMode.None;
|
||||
public string Content { get; set; } = "";
|
||||
public string OldLine { get; set; } = "";
|
||||
public string NewLine { get; set; } = "";
|
||||
public List<HighlightRange> Highlights { get; set; } = new List<HighlightRange>();
|
||||
|
||||
public bool IsContent {
|
||||
get {
|
||||
return Mode == LineMode.Added
|
||||
|| Mode == LineMode.Deleted
|
||||
|| Mode == LineMode.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDifference {
|
||||
get {
|
||||
return Mode == LineMode.Added
|
||||
|| Mode == LineMode.Deleted
|
||||
|| Mode == LineMode.None;
|
||||
}
|
||||
}
|
||||
|
||||
public Line() { }
|
||||
|
||||
public Line(int index, LineMode mode, string content, string oldLine, string newLine) {
|
||||
Index = index;
|
||||
Mode = mode;
|
||||
Content = content;
|
||||
OldLine = oldLine;
|
||||
NewLine = newLine;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBinary = false;
|
||||
public List<Line> Lines = new List<Line>();
|
||||
}
|
||||
}
|
|
@ -1,34 +1,13 @@
|
|||
using System.Collections.Generic;
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// 字串差异对比,改写自DiffPlex
|
||||
/// </summary>
|
||||
public class TextCompare {
|
||||
private static readonly HashSet<char> SEPS = new HashSet<char>(" \t+-*/=!,:;.'\"/?|&#@%`<>()[]{}\\".ToCharArray());
|
||||
|
||||
/// <summary>
|
||||
/// 差异信息
|
||||
/// </summary>
|
||||
public class Different {
|
||||
public int DeletedStart { get; set; }
|
||||
public int DeletedCount { get; set; }
|
||||
public int AddedStart { get; set; }
|
||||
public int AddedCount { get; set; }
|
||||
|
||||
public Different(int dp, int dc, int ap, int ac) {
|
||||
DeletedStart = dp;
|
||||
DeletedCount = dc;
|
||||
AddedStart = ap;
|
||||
AddedCount = ac;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分片
|
||||
/// </summary>
|
||||
public class Chunk {
|
||||
class Chunk {
|
||||
public int Hash;
|
||||
public bool Modified;
|
||||
public int Start;
|
||||
|
@ -42,10 +21,7 @@ namespace SourceGit.Models {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 区间修改状态
|
||||
/// </summary>
|
||||
public enum Edit {
|
||||
enum Edit {
|
||||
None,
|
||||
DeletedRight,
|
||||
DeletedLeft,
|
||||
|
@ -53,10 +29,7 @@ namespace SourceGit.Models {
|
|||
AddedLeft,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前区间检测结果
|
||||
/// </summary>
|
||||
public class EditResult {
|
||||
class EditResult {
|
||||
public Edit State;
|
||||
public int DeleteStart;
|
||||
public int DeleteEnd;
|
||||
|
@ -64,13 +37,14 @@ namespace SourceGit.Models {
|
|||
public int AddEnd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对比字串
|
||||
/// </summary>
|
||||
/// <param name="oldValue"></param>
|
||||
/// <param name="newValue"></param>
|
||||
/// <returns></returns>
|
||||
public static List<Different> Process(string oldValue, string newValue) {
|
||||
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);
|
||||
|
@ -81,10 +55,10 @@ namespace SourceGit.Models {
|
|||
var reverse = new int[max];
|
||||
CheckModified(chunksOld, 0, sizeOld, chunksNew, 0, sizeNew, forward, reverse);
|
||||
|
||||
var ret = new List<Different>();
|
||||
var ret = new List<TextInlineChange>();
|
||||
var posOld = 0;
|
||||
var posNew = 0;
|
||||
var last = null as Different;
|
||||
var last = null as TextInlineChange;
|
||||
do {
|
||||
while (posOld < sizeOld && posNew < sizeNew && !chunksOld[posOld].Modified && !chunksNew[posNew].Modified) {
|
||||
posOld++;
|
||||
|
@ -100,7 +74,7 @@ namespace SourceGit.Models {
|
|||
|
||||
if (countOld + countNew == 0) continue;
|
||||
|
||||
var diff = new Different(
|
||||
var diff = new TextInlineChange(
|
||||
countOld > 0 ? chunksOld[beginOld].Start : 0,
|
||||
countOld,
|
||||
countNew > 0 ? chunksNew[beginNew].Start : 0,
|
||||
|
@ -126,10 +100,11 @@ namespace SourceGit.Models {
|
|||
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 (SEPS.Contains(ch)) {
|
||||
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;
|
||||
|
@ -190,7 +165,6 @@ namespace SourceGit.Models {
|
|||
|
||||
for (int i = 0; i <= half; i++) {
|
||||
|
||||
// 正向
|
||||
for (int j = -i; j <= i; j += 2) {
|
||||
var idx = j + half;
|
||||
int o, n;
|
||||
|
@ -231,7 +205,6 @@ namespace SourceGit.Models {
|
|||
}
|
||||
}
|
||||
|
||||
// 反向
|
||||
for (int j = -i; j <= i; j += 2) {
|
||||
var idx = j + half;
|
||||
int o, n;
|
|
@ -1,9 +0,0 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 文件中的一行内容
|
||||
/// </summary>
|
||||
public class TextLine {
|
||||
public int Number { get; set; }
|
||||
public string Data { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 主题
|
||||
/// </summary>
|
||||
public static class Theme {
|
||||
/// <summary>
|
||||
/// 主题切换事件
|
||||
/// </summary>
|
||||
public static event Action Changed;
|
||||
|
||||
/// <summary>
|
||||
/// 启用主题变化监听
|
||||
/// </summary>
|
||||
/// <param name="elem"></param>
|
||||
public static void AddListener(FrameworkElement elem, Action callback) {
|
||||
elem.Loaded += (_, __) => Changed += callback;
|
||||
elem.Unloaded += (_, __) => Changed -= callback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换主题
|
||||
/// </summary>
|
||||
public static void Change() {
|
||||
var theme = Preference.Instance.General.UseDarkTheme ? "Dark" : "Light";
|
||||
foreach (var rs in App.Current.Resources.MergedDictionaries) {
|
||||
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/", StringComparison.Ordinal)) {
|
||||
rs.Source = new Uri($"pack://application:,,,/Resources/Themes/{theme}.xaml", UriKind.Absolute);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// Git用户
|
||||
/// </summary>
|
||||
public class User {
|
||||
public static User Invalid = new User();
|
||||
public static Dictionary<string, User> Caches = new Dictionary<string, User>();
|
||||
|
@ -12,8 +9,8 @@ namespace SourceGit.Models {
|
|||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
if (obj == null || !(obj is User)) return false;
|
||||
|
||||
if (obj == null || !(obj is User)) return false;
|
||||
|
||||
var other = obj as User;
|
||||
return Name == other.Name && Email == other.Email;
|
||||
}
|
||||
|
|
|
@ -1,268 +1,167 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件系统更新监视
|
||||
/// </summary>
|
||||
public class Watcher {
|
||||
/// <summary>
|
||||
/// 打开仓库事件
|
||||
/// </summary>
|
||||
public static event Action<Repository> Opened;
|
||||
void RefreshBranches();
|
||||
void RefreshTags();
|
||||
void RefreshCommits();
|
||||
void RefreshSubmodules();
|
||||
void RefreshWorkingCopyChanges();
|
||||
void RefreshStashes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仓库的显示名变化了
|
||||
/// </summary>
|
||||
public static event Action<string, string> DisplayNameChanged;
|
||||
public class Watcher : IDisposable {
|
||||
public Watcher(IRepository repo) {
|
||||
_repo = repo;
|
||||
|
||||
/// <summary>
|
||||
/// 仓库的书签变化了
|
||||
/// </summary>
|
||||
public static event Action<string, int> BookmarkChanged;
|
||||
_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;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转到指定提交的事件
|
||||
/// </summary>
|
||||
public event Action<string> Navigate;
|
||||
/// <summary>
|
||||
/// 工作副本变更
|
||||
/// </summary>
|
||||
public event Action WorkingCopyChanged;
|
||||
/// <summary>
|
||||
/// 分支数据变更
|
||||
/// </summary>
|
||||
public event Action BranchChanged;
|
||||
/// <summary>
|
||||
/// 标签变更
|
||||
/// </summary>
|
||||
public event Action TagChanged;
|
||||
/// <summary>
|
||||
/// 贮藏变更
|
||||
/// </summary>
|
||||
public event Action StashChanged;
|
||||
/// <summary>
|
||||
/// 子模块变更
|
||||
/// </summary>
|
||||
public event Action SubmoduleChanged;
|
||||
/// <summary>
|
||||
/// 树更新
|
||||
/// </summary>
|
||||
public event Action SubTreeChanged;
|
||||
_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;
|
||||
|
||||
/// <summary>
|
||||
/// 打开仓库事件
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public static void Open(Repository repo) {
|
||||
if (all.ContainsKey(repo.Path)) {
|
||||
Opened?.Invoke(repo);
|
||||
return;
|
||||
_timer = new Timer(Tick, null, 100, 100);
|
||||
}
|
||||
|
||||
public void SetEnabled(bool enabled) {
|
||||
if (enabled) {
|
||||
if (_lockCount > 0) _lockCount--;
|
||||
} else {
|
||||
_lockCount++;
|
||||
}
|
||||
|
||||
var watcher = new Watcher();
|
||||
watcher.Start(repo.Path, repo.GitDir);
|
||||
all.Add(repo.Path, watcher);
|
||||
repo.LastOpenTime = DateTime.Now.ToFileTime();
|
||||
|
||||
Opened?.Invoke(repo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止指定的监视器
|
||||
/// </summary>
|
||||
/// <param name="repoPath"></param>
|
||||
public static void Close(string repoPath) {
|
||||
if (!all.ContainsKey(repoPath)) return;
|
||||
all[repoPath].Stop();
|
||||
all.Remove(repoPath);
|
||||
public void MarkWorkingCopyRefreshed() {
|
||||
_updateWC = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取得一个仓库的监视器
|
||||
/// </summary>
|
||||
/// <param name="repoPath"></param>
|
||||
/// <returns></returns>
|
||||
public static Watcher Get(string repoPath) {
|
||||
if (all.ContainsKey(repoPath)) return all[repoPath];
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或启用监听
|
||||
/// </summary>
|
||||
/// <param name="repoPath"></param>
|
||||
/// <param name="enabled"></param>
|
||||
public static void SetEnabled(string repoPath, bool enabled) {
|
||||
if (all.ContainsKey(repoPath)) {
|
||||
var watcher = all[repoPath];
|
||||
if (enabled) {
|
||||
if (watcher.lockCount > 0) watcher.lockCount--;
|
||||
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 {
|
||||
watcher.lockCount++;
|
||||
Task.Run(() => {
|
||||
_repo.RefreshBranches();
|
||||
_repo.RefreshCommits();
|
||||
});
|
||||
}
|
||||
|
||||
Task.Run(_repo.RefreshWorkingCopyChanges);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知仓库显示名变化
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public static void NotifyDisplayNameChanged(Repository repo) {
|
||||
DisplayNameChanged?.Invoke(repo.Path, repo.Name);
|
||||
}
|
||||
if (_updateWC > 0 && now > _updateWC) {
|
||||
_updateWC = 0;
|
||||
Task.Run(_repo.RefreshWorkingCopyChanges);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知仓库标签变化
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public static void NotifyBookmarkChanged(Repository repo) {
|
||||
BookmarkChanged?.Invoke(repo.Path, repo.Bookmark);
|
||||
}
|
||||
if (_updateSubmodules > 0 && now > _updateSubmodules) {
|
||||
_updateSubmodules = 0;
|
||||
_repo.RefreshSubmodules();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跳转到指定的提交
|
||||
/// </summary>
|
||||
/// <param name="commit"></param>
|
||||
public void NavigateTo(string commit) {
|
||||
Navigate?.Invoke(commit);
|
||||
}
|
||||
if (_updateStashes > 0 && now > _updateStashes) {
|
||||
_updateStashes = 0;
|
||||
_repo.RefreshStashes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅强制更新本地变化
|
||||
/// </summary>
|
||||
public void RefreshWC() {
|
||||
updateWC = 0;
|
||||
WorkingCopyChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知更新子树列表
|
||||
/// </summary>
|
||||
public void RefreshSubTrees() {
|
||||
SubTreeChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void Start(string repo, string gitDir) {
|
||||
wcWatcher = new FileSystemWatcher();
|
||||
wcWatcher.Path = repo;
|
||||
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 = 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);
|
||||
}
|
||||
|
||||
private void Stop() {
|
||||
repoWatcher.EnableRaisingEvents = false;
|
||||
repoWatcher.Dispose();
|
||||
repoWatcher = null;
|
||||
|
||||
wcWatcher.EnableRaisingEvents = false;
|
||||
wcWatcher.Dispose();
|
||||
wcWatcher = null;
|
||||
|
||||
timer.Dispose();
|
||||
timer = null;
|
||||
|
||||
Navigate = null;
|
||||
WorkingCopyChanged = null;
|
||||
BranchChanged = null;
|
||||
TagChanged = null;
|
||||
StashChanged = null;
|
||||
SubmoduleChanged = null;
|
||||
SubTreeChanged = null;
|
||||
if (_updateTags > 0 && now > _updateTags) {
|
||||
_updateTags = 0;
|
||||
_repo.RefreshTags();
|
||||
_repo.RefreshCommits();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRepositoryChanged(object o, FileSystemEventArgs e) {
|
||||
if (string.IsNullOrEmpty(e.Name)) return;
|
||||
|
||||
if (e.Name.StartsWith("modules", StringComparison.Ordinal)) {
|
||||
updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
} else if (e.Name.StartsWith("refs\\tags", StringComparison.Ordinal)) {
|
||||
updateTags = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
} else if (e.Name.StartsWith("refs\\stash", StringComparison.Ordinal)) {
|
||||
updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
} else if (e.Name.Equals("HEAD", StringComparison.Ordinal) ||
|
||||
e.Name.StartsWith("refs\\heads\\", StringComparison.Ordinal) ||
|
||||
e.Name.StartsWith("refs\\remotes\\", StringComparison.Ordinal) ||
|
||||
e.Name.StartsWith("worktrees\\")) {
|
||||
updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
} else if (e.Name.StartsWith("objects\\", StringComparison.Ordinal) || e.Name.Equals("index", StringComparison.Ordinal)) {
|
||||
updateWC = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
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/")) {
|
||||
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
} else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal)) {
|
||||
_updateWC = DateTime.Now.AddSeconds(.5).ToFileTime();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWorkingCopyChanged(object o, FileSystemEventArgs e) {
|
||||
if (string.IsNullOrEmpty(e.Name)) return;
|
||||
if (e.Name == ".git" || e.Name.StartsWith(".git\\", StringComparison.Ordinal)) return;
|
||||
|
||||
updateWC = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
var name = e.Name.Replace("\\", "/");
|
||||
if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal)) return;
|
||||
if (_updateWC == 0) _updateWC = DateTime.Now.AddSeconds(1).ToFileTime();
|
||||
}
|
||||
|
||||
private void Tick(object sender) {
|
||||
if (lockCount > 0) return;
|
||||
|
||||
var now = DateTime.Now.ToFileTime();
|
||||
if (updateBranch > 0 && now > updateBranch) {
|
||||
BranchChanged?.Invoke();
|
||||
WorkingCopyChanged?.Invoke();
|
||||
updateBranch = 0;
|
||||
updateWC = 0;
|
||||
}
|
||||
|
||||
if (updateWC > 0 && now > updateWC) {
|
||||
WorkingCopyChanged?.Invoke();
|
||||
updateWC = 0;
|
||||
}
|
||||
|
||||
if (updateSubmodules > 0 && now > updateSubmodules) {
|
||||
SubmoduleChanged?.Invoke();
|
||||
updateSubmodules = 0;
|
||||
}
|
||||
|
||||
if (updateStashes > 0 && now > updateStashes) {
|
||||
StashChanged?.Invoke();
|
||||
updateStashes = 0;
|
||||
}
|
||||
|
||||
if (updateTags > 0 && now > updateTags) {
|
||||
TagChanged?.Invoke();
|
||||
updateTags = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#region PRIVATES
|
||||
private static Dictionary<string, Watcher> all = new Dictionary<string, Watcher>();
|
||||
|
||||
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;
|
||||
#endregion
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 应用补丁时空白字符的处理方式
|
||||
/// </summary>
|
||||
public class WhitespaceOption {
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
|
||||
public static List<WhitespaceOption> Supported = new List<WhitespaceOption>() {
|
||||
new WhitespaceOption("Apply.NoWarn", "Apply.NoWarn.Desc", "nowarn"),
|
||||
new WhitespaceOption("Apply.Warn", "Apply.Warn.Desc", "warn"),
|
||||
new WhitespaceOption("Apply.Error", "Apply.Error.Desc", "error"),
|
||||
new WhitespaceOption("Apply.ErrorAll", "Apply.ErrorAll.Desc", "error-all")
|
||||
};
|
||||
|
||||
public WhitespaceOption(string n, string d, string a) {
|
||||
Name = App.Text(n);
|
||||
Desc = App.Text(d);
|
||||
Arg = a;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue