refactor<*>: rewrite all with AvaloniaUI

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

View file

@ -0,0 +1,13 @@
namespace SourceGit.Models {
public class ApplyWhiteSpaceMode {
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public ApplyWhiteSpaceMode(string n, string d, string a) {
Name = App.Text(n);
Desc = App.Text(d);
Arg = a;
}
}
}

120
src/Models/AvatarManager.cs Normal file
View file

@ -0,0 +1,120 @@
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace SourceGit.Models {
public interface IAvatarHost {
void OnAvatarResourceReady(string md5, Bitmap bitmap);
}
public static class AvatarManager {
static AvatarManager() {
_storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit", "avatars");
if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath);
Task.Run(() => {
while (true) {
var md5 = null as string;
lock (_synclock) {
foreach (var one in _requesting) {
md5 = one;
break;
}
}
if (md5 == null) {
Thread.Sleep(100);
continue;
}
var localFile = Path.Combine(_storePath, md5);
var img = null as Bitmap;
try {
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) };
var task = client.GetAsync($"https://cravatar.cn/avatar/{md5}?d=404");
task.Wait();
var rsp = task.Result;
if (rsp.IsSuccessStatusCode) {
using (var stream = rsp.Content.ReadAsStream()) {
using (var writer = File.OpenWrite(localFile)) {
stream.CopyTo(writer);
}
}
using (var reader = File.OpenRead(localFile)) {
img = Bitmap.DecodeToWidth(reader, 128);
}
}
} catch { }
lock (_synclock) {
_requesting.Remove(md5);
}
Dispatcher.UIThread.InvokeAsync(() => {
if (_resources.ContainsKey(md5)) _resources[md5] = img;
else _resources.Add(md5, img);
if (img != null) NotifyResourceReady(md5, img);
});
}
});
}
public static void Subscribe(IAvatarHost host) {
_avatars.Add(new WeakReference<IAvatarHost>(host));
}
public static Bitmap Request(string md5, bool forceRefetch = false) {
if (forceRefetch) {
if (_resources.ContainsKey(md5)) _resources.Remove(md5);
} else {
if (_resources.ContainsKey(md5)) return _resources[md5];
var localFile = Path.Combine(_storePath, md5);
if (File.Exists(localFile)) {
try {
using (var stream = File.OpenRead(localFile)) {
var img = Bitmap.DecodeToWidth(stream, 128);
_resources.Add(md5, img);
return img;
}
} catch { }
}
}
lock (_synclock) {
if (!_requesting.Contains(md5)) _requesting.Add(md5);
}
return null;
}
private static void NotifyResourceReady(string md5, Bitmap bitmap) {
List<WeakReference<IAvatarHost>> invalids = new List<WeakReference<IAvatarHost>>();
foreach (var avatar in _avatars) {
IAvatarHost retrived = null;
if (avatar.TryGetTarget(out retrived)) {
retrived.OnAvatarResourceReady(md5, bitmap);
break;
} else {
invalids.Add(avatar);
}
}
foreach (var invalid in invalids) _avatars.Remove(invalid);
}
private static object _synclock = new object();
private static string _storePath = string.Empty;
private static List<WeakReference<IAvatarHost>> _avatars = new List<WeakReference<IAvatarHost>>();
private static Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
private static HashSet<string> _requesting = new HashSet<string>();
}
}

17
src/Models/Blame.cs Normal file
View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace SourceGit.Models {
public class BlameLineInfo {
public bool IsFirstInGroup { get; set; } = false;
public string CommitSHA { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public string Time { get; set; } = string.Empty;
}
public class BlameData {
public string File { get; set; } = string.Empty;
public List<BlameLineInfo> LineInfos { get; set; } = new List<BlameLineInfo>();
public string Content { get; set; } = string.Empty;
public bool IsBinary { get; set; } = false;
}
}

View file

@ -1,12 +0,0 @@
namespace SourceGit.Models {
/// <summary>
/// 追溯中的行信息
/// </summary>
public class BlameLine {
public string LineNumber { get; set; }
public string CommitSHA { get; set; }
public string Author { get; set; }
public string Time { get; set; }
public string Content { get; set; }
}
}

22
src/Models/Bookmarks.cs Normal file
View file

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace SourceGit.Models {
public static class Bookmarks {
public static readonly Avalonia.Media.IBrush[] Brushes = [
Avalonia.Media.Brushes.Transparent,
Avalonia.Media.Brushes.Red,
Avalonia.Media.Brushes.Orange,
Avalonia.Media.Brushes.Gold,
Avalonia.Media.Brushes.ForestGreen,
Avalonia.Media.Brushes.DarkCyan,
Avalonia.Media.Brushes.DeepSkyBlue,
Avalonia.Media.Brushes.Purple,
];
public static readonly List<int> Supported = new List<int>();
static Bookmarks() {
for (int i = 0; i < Brushes.Length; i++) Supported.Add(i);
}
}
}

View file

@ -1,7 +1,4 @@
namespace SourceGit.Models {
/// <summary>
/// 分支数据
/// </summary>
namespace SourceGit.Models {
public class Branch {
public string Name { get; set; }
public string FullName { get; set; }

View file

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models {
public enum BranchTreeNodeType {
Remote,
Folder,
Branch,
}
public class BranchTreeNode {
public string Name { get; set; }
public BranchTreeNodeType Type { get; set; }
public object Backend { get; set; }
public bool IsExpanded { get; set; }
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
public bool IsUpstreamTrackStatusVisible {
get => IsBranch && !string.IsNullOrEmpty((Backend as Branch).UpstreamTrackStatus);
}
public string UpstreamTrackStatus {
get => Type == BranchTreeNodeType.Branch ? (Backend as Branch).UpstreamTrackStatus : "";
}
public bool IsRemote {
get => Type == BranchTreeNodeType.Remote;
}
public bool IsFolder {
get => Type == BranchTreeNodeType.Folder;
}
public bool IsBranch {
get => Type == BranchTreeNodeType.Branch;
}
public bool IsCurrent {
get => IsBranch && (Backend as Branch).IsCurrent;
}
public class Builder {
public List<BranchTreeNode> Locals => _locals;
public List<BranchTreeNode> Remotes => _remotes;
public void Run(List<Branch> branches, List<Remote> remotes) {
foreach (var remote in remotes) {
var path = $"remote/{remote.Name}";
var node = new BranchTreeNode() {
Name = remote.Name,
Type = BranchTreeNodeType.Remote,
Backend = remote,
IsExpanded = _expanded.Contains(path),
};
_maps.Add(path, node);
_remotes.Add(node);
}
foreach (var branch in branches) {
if (branch.IsLocal) {
MakeBranchNode(branch, _locals, "local");
} else {
var remote = _remotes.Find(x => x.Name == branch.Remote);
if (remote != null) MakeBranchNode(branch, remote.Children, $"remote/{remote.Name}");
}
}
SortNodes(_locals);
SortNodes(_remotes);
}
public void CollectExpandedNodes(List<BranchTreeNode> nodes, bool isLocal) {
CollectExpandedNodes(nodes, isLocal ? "local" : "remote");
}
private void CollectExpandedNodes(List<BranchTreeNode> nodes, string prefix) {
foreach (var node in nodes) {
var path = prefix + "/" + node.Name;
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) _expanded.Add(path);
CollectExpandedNodes(node.Children, path);
}
}
private void MakeBranchNode(Branch branch, List<BranchTreeNode> roots, string prefix) {
var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 1) {
var node = new BranchTreeNode() {
Name = subs[0],
Type = BranchTreeNodeType.Branch,
Backend = branch,
IsExpanded = false,
};
roots.Add(node);
return;
}
BranchTreeNode lastFolder = null;
string path = prefix;
for (int i = 0; i < subs.Length - 1; i++) {
path = string.Concat(path, "/", subs[i]);
if (_maps.ContainsKey(path)) {
lastFolder = _maps[path];
} else if (lastFolder == null) {
lastFolder = new BranchTreeNode() {
Name = subs[i],
Type = BranchTreeNodeType.Folder,
IsExpanded = _expanded.Contains(path),
};
roots.Add(lastFolder);
_maps.Add(path, lastFolder);
} else {
var folder = new BranchTreeNode() {
Name = subs[i],
Type = BranchTreeNodeType.Folder,
IsExpanded = _expanded.Contains(path),
};
_maps.Add(path, folder);
lastFolder.Children.Add(folder);
lastFolder = folder;
}
}
var last = new BranchTreeNode() {
Name = subs[subs.Length - 1],
Type = BranchTreeNodeType.Branch,
Backend = branch,
IsExpanded = false,
};
lastFolder.Children.Add(last);
}
private void SortNodes(List<BranchTreeNode> nodes) {
nodes.Sort((l, r) => {
if (l.Type == r.Type) {
return l.Name.CompareTo(r.Name);
} else {
return (int)(l.Type) - (int)(r.Type);
}
});
foreach (var node in nodes) SortNodes(node.Children);
}
private List<BranchTreeNode> _locals = new List<BranchTreeNode>();
private List<BranchTreeNode> _remotes = new List<BranchTreeNode>();
private HashSet<string> _expanded = new HashSet<string>();
private Dictionary<string, BranchTreeNode> _maps = new Dictionary<string, BranchTreeNode>();
}
}
}

21
src/Models/CRLFMode.cs Normal file
View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace SourceGit.Models {
public class CRLFMode {
public string Name { get; set; }
public string Value { get; set; }
public string Desc { get; set; }
public static List<CRLFMode> Supported = new List<CRLFMode>() {
new CRLFMode("TRUE", "true", "Commit as LF, checkout as CRLF"),
new CRLFMode("INPUT", "input", "Only convert for commit"),
new CRLFMode("FALSE", "false", "Do NOT convert"),
};
public CRLFMode(string name, string value, string desc) {
Name = name;
Value = value;
Desc = desc;
}
}
}

View file

@ -1,25 +0,0 @@
using System.Collections.Generic;
namespace SourceGit.Models {
/// <summary>
/// 自动换行处理方式
/// </summary>
public class CRLFOption {
public string Display { get; set; }
public string Value { get; set; }
public string Desc { get; set; }
public static List<CRLFOption> Supported = new List<CRLFOption>() {
new CRLFOption("TRUE", "true", "Commit as LF, checkout as CRLF"),
new CRLFOption("INPUT", "input", "Only convert for commit"),
new CRLFOption("FALSE", "false", "Do NOT convert"),
};
public CRLFOption(string display, string value, string desc) {
Display = display;
Value = value;
Desc = desc;
}
}
}

View file

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

View file

@ -1,16 +1,10 @@
using Avalonia;
using System;
using System.Collections.Generic;
using System.Windows;
namespace SourceGit.Models {
/// <summary>
/// 提交记录
/// </summary>
public class Commit {
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
public string SHA { get; set; } = string.Empty;
public string ShortSHA => SHA.Substring(0, 8);
public User Author { get; set; } = User.Invalid;
public ulong AuthorTime { get; set; } = 0;
public User Committer { get; set; } = User.Invalid;
@ -23,10 +17,18 @@ namespace SourceGit.Models {
public bool IsMerged { get; set; } = false;
public Thickness Margin { get; set; } = new Thickness(0);
public string AuthorTimeStr => UTC_START.AddSeconds(AuthorTime).ToString("yyyy-MM-dd HH:mm:ss");
public string CommitterTimeStr => UTC_START.AddSeconds(CommitterTime).ToString("yyyy-MM-dd HH:mm:ss");
public string AuthorTimeShortStr => UTC_START.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
public string CommitterTimeShortStr => UTC_START.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
public string AuthorTimeStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd HH:mm:ss");
public string CommitterTimeStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd HH:mm:ss");
public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
public bool IsCommitterVisible {
get => Author != Committer || AuthorTime != CommitterTime;
}
public string FullMessage {
get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}";
}
public static void ParseUserAndTime(string data, ref User user, ref ulong time) {
var userEndIdx = data.IndexOf('>');
@ -36,5 +38,7 @@ namespace SourceGit.Models {
user = User.FindOrAdd(data.Substring(0, userEndIdx));
time = timeEndIdx < 0 ? 0 : ulong.Parse(data.Substring(userEndIdx + 2, timeEndIdx - userEndIdx - 2));
}
private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
}
}

205
src/Models/CommitGraph.cs Normal file
View file

@ -0,0 +1,205 @@
using Avalonia;
using System;
using System.Collections.Generic;
namespace SourceGit.Models {
public class CommitGraph {
public class Path {
public List<Point> Points = new List<Point>();
public int Color = 0;
}
public class PathHelper {
public string Next;
public bool IsMerged;
public double LastX;
public double LastY;
public double EndY;
public Path Path;
public PathHelper(string next, bool isMerged, int color, Point start) {
Next = next;
IsMerged = isMerged;
LastX = start.X;
LastY = start.Y;
EndY = LastY;
Path = new Path();
Path.Color = color;
Path.Points.Add(start);
}
public PathHelper(string next, bool isMerged, int color, Point start, Point to) {
Next = next;
IsMerged = isMerged;
LastX = to.X;
LastY = to.Y;
EndY = LastY;
Path = new Path();
Path.Color = color;
Path.Points.Add(start);
Path.Points.Add(to);
}
public void Add(double x, double y, double halfHeight, bool isEnd = false) {
if (x > LastX) {
Add(new Point(LastX, LastY));
Add(new Point(x, y - halfHeight));
if (isEnd) Add(new Point(x, y));
} else if (x < LastX) {
if (y > LastY + halfHeight) Add(new Point(LastX, LastY + halfHeight));
Add(new Point(x, y));
} else if (isEnd) {
Add(new Point(x, y));
}
LastX = x;
LastY = y;
}
private void Add(Point p) {
if (EndY < p.Y) {
Path.Points.Add(p);
EndY = p.Y;
}
}
}
public class Link {
public Point Start;
public Point Control;
public Point End;
public int Color;
}
public class Dot {
public Point Center;
public int Color;
}
public List<Path> Paths { get; set; } = new List<Path>();
public List<Link> Links { get; set; } = new List<Link>();
public List<Dot> Dots { get; set; } = new List<Dot>();
public static CommitGraph Parse(List<Commit> commits, double rowHeight, int colorCount) {
double UNIT_WIDTH = 12;
double HALF_WIDTH = 6;
double UNIT_HEIGHT = rowHeight;
double HALF_HEIGHT = rowHeight / 2;
var temp = new CommitGraph();
var unsolved = new List<PathHelper>();
var mapUnsolved = new Dictionary<string, PathHelper>();
var ended = new List<PathHelper>();
var offsetY = -HALF_HEIGHT;
var colorIdx = 0;
foreach (var commit in commits) {
var major = null as PathHelper;
var isMerged = commit.IsMerged;
var oldCount = unsolved.Count;
// Update current y offset
offsetY += UNIT_HEIGHT;
// Find first curves that links to this commit and marks others that links to this commit ended.
double offsetX = -HALF_WIDTH;
foreach (var l in unsolved) {
if (l.Next == commit.SHA) {
if (major == null) {
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0) {
major.Next = commit.Parents[0];
if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major);
} else {
major.Next = "ENDED";
ended.Add(l);
}
major.Add(offsetX, offsetY, HALF_HEIGHT);
} else {
ended.Add(l);
}
isMerged = isMerged || l.IsMerged;
} else {
if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.Add(offsetX, offsetY, HALF_HEIGHT);
}
}
// Create new curve for branch head
if (major == null && commit.Parents.Count > 0) {
offsetX += UNIT_WIDTH;
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major);
temp.Paths.Add(major.Path);
colorIdx = (colorIdx + 1) % colorCount;
}
// Calculate link position of this commit.
Point position = new Point(offsetX, offsetY);
if (major != null) {
major.IsMerged = isMerged;
position = new Point(major.LastX, offsetY);
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color });
} else {
temp.Dots.Add(new Dot() { Center = position, Color = 0 });
}
// Deal with parents
for (int j = 1; j < commit.Parents.Count; j++) {
var parent = commit.Parents[j];
if (mapUnsolved.ContainsKey(parent)) {
var l = mapUnsolved[parent];
var link = new Link();
link.Start = position;
link.End = new Point(l.LastX, offsetY + HALF_HEIGHT);
link.Control = new Point(link.End.X, link.Start.Y);
link.Color = l.Path.Color;
temp.Links.Add(link);
} else {
offsetX += UNIT_WIDTH;
// Create new curve for parent commit that not includes before
var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT));
unsolved.Add(l);
temp.Paths.Add(l.Path);
colorIdx = (colorIdx + 1) % colorCount;
}
}
// Remove ended curves from unsolved
foreach (var l in ended) {
l.Add(position.X, position.Y, HALF_HEIGHT, true);
unsolved.Remove(l);
}
// Margins & merge state (used by datagrid).
commit.IsMerged = isMerged;
commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH), 0, 0, 0);
// Clean up
ended.Clear();
mapUnsolved.Clear();
}
// Deal with curves haven't ended yet.
for (int i = 0; i < unsolved.Count; i++) {
var path = unsolved[i];
var endY = (commits.Count - 0.5) * UNIT_HEIGHT;
if (path.Path.Points.Count == 1 && path.Path.Points[0].Y == endY) continue;
path.Add((i + 0.5) * UNIT_WIDTH, endY + HALF_HEIGHT, HALF_HEIGHT, true);
}
unsolved.Clear();
return temp;
}
}
}

View file

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

View file

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

View file

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

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

View file

@ -1,9 +0,0 @@
namespace SourceGit.Models {
/// <summary>
/// 文件大小变化
/// </summary>
public class FileSizeChange {
public long OldSize = 0;
public long NewSize = 0;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +0,0 @@
namespace SourceGit.Models {
/// <summary>
/// 文件中的一行内容
/// </summary>
public class TextLine {
public int Number { get; set; }
public string Data { get; set; }
}
}

View file

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

View file

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

View file

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

View file

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