mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
refactor<*>: rewrite all codes...
This commit is contained in:
parent
89ff8aa744
commit
30ab8ae954
342 changed files with 17208 additions and 19633 deletions
|
@ -7,7 +7,7 @@
|
|||
[发行版](https://gitee.com/sourcegit/SourceGit/releases/)
|
||||
|
||||
* `SourceGit.exe`为不带.NET 5.0运行时的可执行文件,需要先安装.NET 5
|
||||
* `SourceGit.zip`为自带.NET 5.0的可执行文件
|
||||
* `SourceGit_48.exe`为.NET 4.8编译的可执行文件,Window 10 已内置该运行时
|
||||
|
||||
## 预览
|
||||
|
||||
|
|
13
build.bat
13
build.bat
|
@ -1,9 +1,16 @@
|
|||
rmdir /s /q publish
|
||||
|
||||
cd src
|
||||
rmdir /s /q bin
|
||||
rmdir /s /q obj
|
||||
dotnet publish --nologo -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained -o ../publish
|
||||
dotnet publish SourceGit.csproj --nologo -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained -o ../publish
|
||||
|
||||
rmdir /s /q bin
|
||||
rmdir /s /q obj
|
||||
dotnet publish --nologo -c Release -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained -o ../publish/SourceGit
|
||||
cd ..
|
||||
dotnet publish SourceGit_48.csproj --nologo -c Release -r win-x64 -o ../publish/net48
|
||||
|
||||
cd ../publish
|
||||
ilrepack /ndebug /out:SourceGit_48.exe net48/SourceGit.exe net48/Newtonsoft.Json.dll
|
||||
rmdir /s /q net48
|
||||
|
||||
cd ../
|
|
@ -1,302 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit {
|
||||
|
||||
/// <summary>
|
||||
/// User's preference settings. Serialized to
|
||||
/// </summary>
|
||||
public class Preference {
|
||||
|
||||
/// <summary>
|
||||
/// Tools setting.
|
||||
/// </summary>
|
||||
public class ToolSetting {
|
||||
/// <summary>
|
||||
/// Git executable file path.
|
||||
/// </summary>
|
||||
public string GitExecutable { get; set; }
|
||||
/// <summary>
|
||||
/// Default clone directory.
|
||||
/// </summary>
|
||||
public string GitDefaultCloneDir { get; set; }
|
||||
/// <summary>
|
||||
/// Selected merge tool.
|
||||
/// </summary>
|
||||
public int MergeTool { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// Executable file path for merge tool.
|
||||
/// </summary>
|
||||
public string MergeExecutable { get; set; } = "--";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File's display mode.
|
||||
/// </summary>
|
||||
public enum FilesDisplayMode {
|
||||
Tree,
|
||||
List,
|
||||
Grid,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for UI.
|
||||
/// </summary>
|
||||
public class UISetting {
|
||||
/// <summary>
|
||||
/// Use light theme?
|
||||
/// </summary>
|
||||
public bool UseLightTheme { get; set; }
|
||||
/// <summary>
|
||||
/// Locale
|
||||
/// </summary>
|
||||
public string Locale { get; set; } = "en_US";
|
||||
/// <summary>
|
||||
/// Base URL to get avatar
|
||||
/// </summary>
|
||||
public string AvatarServer { get; set; } = "https://www.gravatar.com/avatar/";
|
||||
/// <summary>
|
||||
/// Main window width
|
||||
/// </summary>
|
||||
public double WindowWidth { get; set; }
|
||||
/// <summary>
|
||||
/// Main window height
|
||||
/// </summary>
|
||||
public double WindowHeight { get; set; }
|
||||
/// <summary>
|
||||
/// Move commit viewer from bottom to right
|
||||
/// </summary>
|
||||
public bool MoveCommitViewerRight { get; set; }
|
||||
/// <summary>
|
||||
/// File's display mode in unstaged view.
|
||||
/// </summary>
|
||||
public FilesDisplayMode UnstageFileDisplayMode { get; set; }
|
||||
/// <summary>
|
||||
/// File's display mode in staged view.
|
||||
/// </summary>
|
||||
public FilesDisplayMode StagedFileDisplayMode { get; set; }
|
||||
/// <summary>
|
||||
/// Use DataGrid instead of TreeView in changes view.
|
||||
/// </summary>
|
||||
public bool UseListInChanges { get; set; }
|
||||
/// <summary>
|
||||
/// Use combined instead of side-by-side mode in diff viewer.
|
||||
/// </summary>
|
||||
public bool UseCombinedDiff { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group(Virtual folder) for watched repositories.
|
||||
/// </summary>
|
||||
public class Group {
|
||||
/// <summary>
|
||||
/// Unique ID of this group.
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
/// <summary>
|
||||
/// Display name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Parent ID.
|
||||
/// </summary>
|
||||
public string ParentId { get; set; }
|
||||
/// <summary>
|
||||
/// Cache UI IsExpended status.
|
||||
/// </summary>
|
||||
public bool IsExpended { get; set; }
|
||||
}
|
||||
|
||||
#region SAVED_DATAS
|
||||
/// <summary>
|
||||
/// Check for updates.
|
||||
/// </summary>
|
||||
public bool CheckUpdate { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Last UNIX timestamp to check for update.
|
||||
/// </summary>
|
||||
public int LastCheckUpdate { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// Fetch remotes automatically?
|
||||
/// </summary>
|
||||
public bool AutoFetchRemotes { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Settings for executables.
|
||||
/// </summary>
|
||||
public ToolSetting Tools { get; set; } = new ToolSetting();
|
||||
/// <summary>
|
||||
/// Use light color theme.
|
||||
/// </summary>
|
||||
public UISetting UI { get; set; } = new UISetting();
|
||||
#endregion
|
||||
|
||||
#region SETTING_REPOS
|
||||
/// <summary>
|
||||
/// Groups for repositories.
|
||||
/// </summary>
|
||||
public List<Group> Groups { get; set; } = new List<Group>();
|
||||
/// <summary>
|
||||
/// Watched repositories.
|
||||
/// </summary>
|
||||
public List<Git.Repository> Repositories { get; set; } = new List<Git.Repository>();
|
||||
#endregion
|
||||
|
||||
#region METHODS_ON_GROUP
|
||||
/// <summary>
|
||||
/// Add new group(virtual folder).
|
||||
/// </summary>
|
||||
/// <param name="name">Display name.</param>
|
||||
/// <param name="parentId">Parent group ID.</param>
|
||||
/// <returns>Added group instance.</returns>
|
||||
public Group AddGroup(string name, string parentId) {
|
||||
var group = new Group() {
|
||||
Name = name,
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ParentId = parentId,
|
||||
IsExpended = false,
|
||||
};
|
||||
|
||||
Groups.Add(group);
|
||||
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find group by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Unique ID</param>
|
||||
/// <returns>Founded group's instance.</returns>
|
||||
public Group FindGroup(string id) {
|
||||
foreach (var group in Groups) {
|
||||
if (group.Id == id) return group;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename group.
|
||||
/// </summary>
|
||||
/// <param name="id">Unique ID</param>
|
||||
/// <param name="newName">New name.</param>
|
||||
public void RenameGroup(string id, string newName) {
|
||||
foreach (var group in Groups) {
|
||||
if (group.Id == id) {
|
||||
group.Name = newName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a group.
|
||||
/// </summary>
|
||||
/// <param name="id">Unique ID</param>
|
||||
public void RemoveGroup(string id) {
|
||||
int removedIdx = -1;
|
||||
|
||||
for (int i = 0; i < Groups.Count; i++) {
|
||||
if (Groups[i].Id == id) {
|
||||
removedIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedIdx >= 0) Groups.RemoveAt(removedIdx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if given group has relations.
|
||||
/// </summary>
|
||||
/// <param name="parentId"></param>
|
||||
/// <param name="subId"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsSubGroup(string parentId, string subId) {
|
||||
if (string.IsNullOrEmpty(parentId)) return false;
|
||||
if (parentId == subId) return true;
|
||||
|
||||
var g = FindGroup(subId);
|
||||
if (g == null) return false;
|
||||
|
||||
g = FindGroup(g.ParentId);
|
||||
while (g != null) {
|
||||
if (g.Id == parentId) return true;
|
||||
g = FindGroup(g.ParentId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region METHODS_ON_REPOS
|
||||
/// <summary>
|
||||
/// Add repository.
|
||||
/// </summary>
|
||||
/// <param name="path">Local storage path.</param>
|
||||
/// <param name="groupId">Group's ID</param>
|
||||
/// <returns>Added repository instance.</returns>
|
||||
public Git.Repository AddRepository(string path, string groupId) {
|
||||
var repo = FindRepository(path);
|
||||
if (repo != null) return repo;
|
||||
|
||||
var dir = new DirectoryInfo(path);
|
||||
repo = new Git.Repository() {
|
||||
Path = dir.FullName,
|
||||
Name = dir.Name,
|
||||
GroupId = groupId,
|
||||
};
|
||||
|
||||
Repositories.Add(repo);
|
||||
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
return repo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find repository by path.
|
||||
/// </summary>
|
||||
/// <param name="path">Local storage path.</param>
|
||||
/// <returns>Founded repository instance.</returns>
|
||||
public Git.Repository FindRepository(string path) {
|
||||
var dir = new DirectoryInfo(path);
|
||||
foreach (var repo in Repositories) {
|
||||
if (repo.Path == dir.FullName) return repo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change a repository's display name in RepositoryManager.
|
||||
/// </summary>
|
||||
/// <param name="path">Local storage path.</param>
|
||||
/// <param name="newName">New name</param>
|
||||
public void RenameRepository(string path, string newName) {
|
||||
var repo = FindRepository(path);
|
||||
if (repo == null) return;
|
||||
|
||||
repo.Name = newName;
|
||||
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a repository in RepositoryManager.
|
||||
/// </summary>
|
||||
/// <param name="path">Local storage path.</param>
|
||||
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
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Icons.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Controls.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Themes/Dark.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Themes/Light.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Locales/en_US.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
|
213
src/App.xaml.cs
213
src/App.xaml.cs
|
@ -1,10 +1,8 @@
|
|||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
@ -12,194 +10,83 @@ using System.Windows;
|
|||
namespace SourceGit {
|
||||
|
||||
/// <summary>
|
||||
/// Application.
|
||||
/// 程序入口.
|
||||
/// </summary>
|
||||
public partial class App : Application {
|
||||
/// <summary>
|
||||
/// Getter/Setter for application user setting.
|
||||
/// </summary>
|
||||
public static Preference Setting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Check if GIT has been configured.
|
||||
/// 读取本地化字串
|
||||
/// </summary>
|
||||
public static bool IsGitConfigured {
|
||||
get {
|
||||
return !string.IsNullOrEmpty(Setting.Tools.GitExecutable)
|
||||
&& File.Exists(Setting.Tools.GitExecutable);
|
||||
}
|
||||
/// <param name="key">本地化字串的Key</param>
|
||||
/// <param name="args">可选格式化参数</param>
|
||||
/// <returns>本地化字串</returns>
|
||||
public static string Text(string key, params object[] args) {
|
||||
var data = Current.FindResource($"Text.{key}") as string;
|
||||
if (string.IsNullOrEmpty(data)) return $"Text.{key}";
|
||||
return string.Format(data, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load text from locales.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public static string Text(string key) {
|
||||
return Current.FindResource("Text." + key) as string;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format text
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static string Format(string key, params object[] args) {
|
||||
return string.Format(Text(key), args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise error message.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public static void RaiseError(string msg) {
|
||||
Current.Dispatcher.Invoke(() => {
|
||||
(Current.MainWindow as UI.Launcher).Errors.Add(msg);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open repository.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public static void Open(Git.Repository repo) {
|
||||
(Current.MainWindow as UI.Launcher).Open(repo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save settings.
|
||||
/// </summary>
|
||||
public static void SaveSetting() {
|
||||
var settingFile = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"SourceGit",
|
||||
"preference.json");
|
||||
|
||||
var dir = Path.GetDirectoryName(settingFile);
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
var data = JsonSerializer.Serialize(Setting, new JsonSerializerOptions() { WriteIndented = true });
|
||||
File.WriteAllText(settingFile, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startup event.
|
||||
/// 启动.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnAppStartup(object sender, StartupEventArgs e) {
|
||||
// Use this app as a sequence editor?
|
||||
if (OpenAsEditor(e)) return;
|
||||
|
||||
// Load settings.
|
||||
var settingFile = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"SourceGit",
|
||||
"preference.json");
|
||||
if (!File.Exists(settingFile)) {
|
||||
Setting = new Preference();
|
||||
} else {
|
||||
Setting = JsonSerializer.Deserialize<Preference>(File.ReadAllText(settingFile));
|
||||
// 创建必要目录
|
||||
if (!Directory.Exists(Views.Controls.Avatar.CACHE_PATH)) {
|
||||
Directory.CreateDirectory(Views.Controls.Avatar.CACHE_PATH);
|
||||
}
|
||||
|
||||
// Make sure avatar cache folder exists
|
||||
if (!Directory.Exists(Helpers.Avatar.CACHE_PATH)) Directory.CreateDirectory(Helpers.Avatar.CACHE_PATH);
|
||||
|
||||
// Try auto configure git via registry.
|
||||
if (Setting == null || !IsGitConfigured) {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var git = root.OpenSubKey("SOFTWARE\\GitForWindows");
|
||||
if (git != null) {
|
||||
Setting.Tools.GitExecutable = Path.Combine(
|
||||
git.GetValue("InstallPath") as string,
|
||||
"bin",
|
||||
"git.exe");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply themes
|
||||
if (Setting.UI.UseLightTheme) {
|
||||
// 控制主题
|
||||
if (Models.Preference.Instance.General.UseDarkTheme) {
|
||||
foreach (var rs in Current.Resources.MergedDictionaries) {
|
||||
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/")) {
|
||||
rs.Source = new Uri("pack://application:,,,/Resources/Themes/Light.xaml", UriKind.Absolute);
|
||||
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/", StringComparison.Ordinal)) {
|
||||
rs.Source = new Uri("pack://application:,,,/Resources/Themes/Dark.xaml", UriKind.Absolute);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply locales
|
||||
if (Setting.UI.Locale != "en_US") {
|
||||
// 控制显示语言
|
||||
var lang = Models.Preference.Instance.General.Locale;
|
||||
if (lang != "en_US") {
|
||||
foreach (var rs in Current.Resources.MergedDictionaries) {
|
||||
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Locales/")) {
|
||||
rs.Source = new Uri($"pack://application:,,,/Resources/Locales/{Setting.UI.Locale}.xaml", UriKind.Absolute);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show main window
|
||||
if (e.Args.Length == 1) {
|
||||
MainWindow = new UI.Launcher(e.Args[0]);
|
||||
} else {
|
||||
MainWindow = new UI.Launcher(null);
|
||||
}
|
||||
// 主界面显示
|
||||
MainWindow = new Views.Launcher();
|
||||
MainWindow.Show();
|
||||
|
||||
|
||||
// Check for update.
|
||||
if (Setting.CheckUpdate && Setting.LastCheckUpdate != DateTime.Now.DayOfYear) {
|
||||
Setting.LastCheckUpdate = DateTime.Now.DayOfYear;
|
||||
SaveSetting();
|
||||
Task.Run(CheckUpdate);
|
||||
// 如果启动命令中指定了路径,打开指定目录的仓库
|
||||
if (e.Args.Length > 0) {
|
||||
var repo = Models.Preference.Instance.FindRepository(e.Args[0]);
|
||||
if (repo == null) {
|
||||
var path = new Commands.GetRepositoryRootPath(e.Args[0]).Result();
|
||||
if (path != null) {
|
||||
var gitDir = new Commands.QueryGitDir(path).Result();
|
||||
repo = Models.Preference.Instance.AddRepository(path, gitDir, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivated event.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnAppDeactivated(object sender, EventArgs e) {
|
||||
GC.Collect();
|
||||
SaveSetting();
|
||||
if (repo != null) Models.Watcher.Open(repo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to open app as git editor
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
/// <returns></returns>
|
||||
private bool OpenAsEditor(StartupEventArgs e) {
|
||||
if (e.Args.Length < 3) return false;
|
||||
|
||||
switch (e.Args[0]) {
|
||||
case "--sequence":
|
||||
var output = File.CreateText(e.Args[2]);
|
||||
output.Write(File.ReadAllText(e.Args[1]));
|
||||
output.Flush();
|
||||
output.Close();
|
||||
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for update.
|
||||
/// </summary>
|
||||
private void CheckUpdate() {
|
||||
// 检测更新
|
||||
if (Models.Preference.Instance.General.CheckForUpdate) {
|
||||
var curDayOfYear = DateTime.Now.DayOfYear;
|
||||
var lastDayOfYear = Models.Preference.Instance.General.LastCheckDay;
|
||||
if (lastDayOfYear != curDayOfYear) {
|
||||
Models.Preference.Instance.General.LastCheckDay = curDayOfYear;
|
||||
Task.Run(() => {
|
||||
try {
|
||||
var web = new WebClient() { Encoding = Encoding.UTF8 };
|
||||
var raw = web.DownloadString("https://gitee.com/api/v5/repos/sourcegit/SourceGit/releases/latest");
|
||||
var ver = JsonSerializer.Deserialize<Git.Version>(raw);
|
||||
var ver = Models.Version.Load(raw);
|
||||
var cur = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
|
||||
var matches = Regex.Match(ver.TagName, @"^v(\d+)\.(\d+).*");
|
||||
|
@ -208,15 +95,21 @@ namespace SourceGit {
|
|||
var major = int.Parse(matches.Groups[1].Value);
|
||||
var minor = int.Parse(matches.Groups[2].Value);
|
||||
if (major > cur.Major || (major == cur.Major && minor > cur.Minor)) {
|
||||
Dispatcher.Invoke(() => {
|
||||
var dialog = new UI.UpdateAvailable(ver);
|
||||
dialog.Owner = MainWindow;
|
||||
dialog.ShowDialog();
|
||||
Dispatcher.Invoke(() => Views.Upgrade.Open(MainWindow, ver));
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// IGNORE
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台运行
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnAppDeactivated(object sender, EventArgs e) {
|
||||
Models.Preference.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
src/Commands/Add.cs
Normal file
27
src/Commands/Add.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// `git add`命令
|
||||
/// </summary>
|
||||
public class Add : Command {
|
||||
public Add(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "add .";
|
||||
}
|
||||
|
||||
public Add(string repo, List<string> paths) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("add --");
|
||||
foreach (var p in paths) {
|
||||
builder.Append(" \"");
|
||||
builder.Append(p);
|
||||
builder.Append("\"");
|
||||
}
|
||||
|
||||
Cwd = repo;
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
15
src/Commands/Apply.cs
Normal file
15
src/Commands/Apply.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 应用Patch
|
||||
/// </summary>
|
||||
public class Apply : Command {
|
||||
|
||||
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode) {
|
||||
Cwd = repo;
|
||||
Args = "apply ";
|
||||
if (ignoreWhitespace) Args += "--ignore-whitespace ";
|
||||
else Args += $"--whitespace={whitespaceMode} ";
|
||||
Args += $"\"{file}\"";
|
||||
}
|
||||
}
|
||||
}
|
58
src/Commands/Blame.cs
Normal file
58
src/Commands/Blame.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 逐行追溯
|
||||
/// </summary>
|
||||
public class Blame : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)");
|
||||
private Data data = new Data();
|
||||
|
||||
public class Data {
|
||||
public List<Models.BlameLine> Lines = new List<Models.BlameLine>();
|
||||
public bool IsBinary = false;
|
||||
}
|
||||
|
||||
public Blame(string repo, string file, string revision) {
|
||||
Cwd = repo;
|
||||
Args = $"blame -t {revision} -- \"{file}\"";
|
||||
}
|
||||
|
||||
public Data Result() {
|
||||
Exec();
|
||||
return data;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (data.IsBinary) return;
|
||||
if (string.IsNullOrEmpty(line)) return;
|
||||
|
||||
if (line.IndexOf('\0') >= 0) {
|
||||
data.IsBinary = true;
|
||||
data.Lines.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var commit = match.Groups[1].Value;
|
||||
var author = match.Groups[2].Value;
|
||||
var timestamp = int.Parse(match.Groups[3].Value);
|
||||
var content = match.Groups[4].Value;
|
||||
var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
var blameLine = new Models.BlameLine() {
|
||||
LineNumber = $"{data.Lines.Count+1}",
|
||||
CommitSHA = commit,
|
||||
Author = author,
|
||||
Time = when,
|
||||
Content = content,
|
||||
};
|
||||
|
||||
data.Lines.Add(blameLine);
|
||||
}
|
||||
}
|
||||
}
|
33
src/Commands/Branch.cs
Normal file
33
src/Commands/Branch.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 分支相关操作
|
||||
/// </summary>
|
||||
class Branch : Command {
|
||||
private string target = null;
|
||||
|
||||
public Branch(string repo, string branch) {
|
||||
Cwd = repo;
|
||||
target = branch;
|
||||
}
|
||||
|
||||
public void Create(string basedOn) {
|
||||
Args = $"branch {target} {basedOn}";
|
||||
Exec();
|
||||
}
|
||||
|
||||
public void Rename(string to) {
|
||||
Args = $"branch -M {target} {to}";
|
||||
Exec();
|
||||
}
|
||||
|
||||
public void SetUpstream(string upstream) {
|
||||
Args = $"branch {target} -u {upstream}";
|
||||
Exec();
|
||||
}
|
||||
|
||||
public void Delete() {
|
||||
Args = $"branch -D {target}";
|
||||
Exec();
|
||||
}
|
||||
}
|
||||
}
|
78
src/Commands/Branches.cs
Normal file
78
src/Commands/Branches.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 解析所有的分支
|
||||
/// </summary>
|
||||
public class Branches : Command {
|
||||
private static readonly string PREFIX_LOCAL = "refs/heads/";
|
||||
private static readonly string PREFIX_REMOTE = "refs/remotes/";
|
||||
private static readonly string CMD = "branch -l --all -v --format=\"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)\"";
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
|
||||
private static readonly Regex REG_AHEAD = new Regex(@"ahead (\d+)");
|
||||
private static readonly Regex REG_BEHIND = new Regex(@"behind (\d+)");
|
||||
|
||||
private List<Models.Branch> loaded = new List<Models.Branch>();
|
||||
|
||||
public Branches(string path) {
|
||||
Cwd = path;
|
||||
Args = CMD;
|
||||
}
|
||||
|
||||
public List<Models.Branch> Result() {
|
||||
Exec();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var branch = new Models.Branch();
|
||||
var refName = match.Groups[1].Value;
|
||||
if (refName.EndsWith("/HEAD")) return;
|
||||
|
||||
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) {
|
||||
branch.Name = refName.Substring(PREFIX_LOCAL.Length);
|
||||
branch.IsLocal = true;
|
||||
} else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal)) {
|
||||
var name = refName.Substring(PREFIX_REMOTE.Length);
|
||||
branch.Remote = name.Substring(0, name.IndexOf('/'));
|
||||
branch.Name = name.Substring(branch.Remote.Length + 1);
|
||||
branch.IsLocal = false;
|
||||
} else {
|
||||
branch.Name = refName;
|
||||
branch.IsLocal = true;
|
||||
}
|
||||
|
||||
branch.FullName = refName;
|
||||
branch.Head = match.Groups[2].Value;
|
||||
branch.IsCurrent = match.Groups[3].Value == "*";
|
||||
branch.Upstream = match.Groups[4].Value;
|
||||
branch.UpstreamTrackStatus = ParseTrackStatus(match.Groups[5].Value);
|
||||
branch.HeadSubject = match.Groups[6].Value;
|
||||
|
||||
loaded.Add(branch);
|
||||
}
|
||||
|
||||
private string ParseTrackStatus(string data) {
|
||||
if (string.IsNullOrEmpty(data)) return "";
|
||||
|
||||
string track = "";
|
||||
|
||||
var ahead = REG_AHEAD.Match(data);
|
||||
if (ahead.Success) {
|
||||
track += ahead.Groups[1].Value + "↑ ";
|
||||
}
|
||||
|
||||
var behind = REG_BEHIND.Match(data);
|
||||
if (behind.Success) {
|
||||
track += behind.Groups[1].Value + "↓";
|
||||
}
|
||||
|
||||
return track.Trim();
|
||||
}
|
||||
}
|
||||
}
|
46
src/Commands/Checkout.cs
Normal file
46
src/Commands/Checkout.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 检出
|
||||
/// </summary>
|
||||
public class Checkout : Command {
|
||||
|
||||
public Checkout(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Branch(string branch) {
|
||||
Args = $"checkout {branch}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Branch(string branch, string basedOn) {
|
||||
Args = $"checkout -b {branch} {basedOn}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool File(string file, bool useTheirs) {
|
||||
if (useTheirs) {
|
||||
Args = $"checkout --theirs -- \"{file}\"";
|
||||
} else {
|
||||
Args = $"checkout --ours -- \"{file}\"";
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Files(List<string> files) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("checkout -qf --");
|
||||
foreach (var f in files) {
|
||||
builder.Append(" \"");
|
||||
builder.Append(f);
|
||||
builder.Append("\"");
|
||||
}
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
13
src/Commands/CherryPick.cs
Normal file
13
src/Commands/CherryPick.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 遴选命令
|
||||
/// </summary>
|
||||
public class CherryPick : Command {
|
||||
|
||||
public CherryPick(string repo, string commit, bool noCommit) {
|
||||
var mode = noCommit ? "-n" : "--ff";
|
||||
Cwd = repo;
|
||||
Args = $"cherry-pick {mode} {commit}";
|
||||
}
|
||||
}
|
||||
}
|
12
src/Commands/Clean.cs
Normal file
12
src/Commands/Clean.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 清理指令
|
||||
/// </summary>
|
||||
public class Clean : Command {
|
||||
|
||||
public Clean(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "clean -qfd";
|
||||
}
|
||||
}
|
||||
}
|
26
src/Commands/Clone.cs
Normal file
26
src/Commands/Clone.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
/// </summary>
|
||||
public class Clone : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Clone(string path, string url, string localName, string extraArgs, Action<string> outputHandler) {
|
||||
Cwd = path;
|
||||
TraitErrorAsOutput = true;
|
||||
Args = "-c credential.helper=manager clone --progress --verbose --recurse-submodules ";
|
||||
handler = outputHandler;
|
||||
|
||||
if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} ";
|
||||
Args += $"{url} ";
|
||||
if (!string.IsNullOrEmpty(localName)) Args += localName;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
}
|
||||
}
|
161
src/Commands/Command.cs
Normal file
161
src/Commands/Command.cs
Normal file
|
@ -0,0 +1,161 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取消命令执行的对象
|
||||
/// </summary>
|
||||
public class Cancellable {
|
||||
public bool IsCancelRequested { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命令接口
|
||||
/// </summary>
|
||||
public class Command {
|
||||
|
||||
/// <summary>
|
||||
/// 读取全部输出时的结果
|
||||
/// </summary>
|
||||
public class ReadToEndResult {
|
||||
public bool IsSuccess { get; set; }
|
||||
public string Output { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行路径
|
||||
/// </summary>
|
||||
public string Cwd { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 参数
|
||||
/// </summary>
|
||||
public string Args { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 使用标准错误输出
|
||||
/// </summary>
|
||||
public bool TraitErrorAsOutput { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 用于取消命令指行的Token
|
||||
/// </summary>
|
||||
public Cancellable Token { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 运行
|
||||
/// </summary>
|
||||
public bool Exec() {
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = Models.Preference.Instance.Git.Path;
|
||||
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.RedirectStandardOutput = true;
|
||||
start.RedirectStandardError = true;
|
||||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
|
||||
|
||||
var progressFilter = new Regex(@"\d+\%");
|
||||
var errs = new List<string>();
|
||||
var proc = new Process() { StartInfo = start };
|
||||
var isCancelled = false;
|
||||
|
||||
proc.OutputDataReceived += (o, e) => {
|
||||
if (Token != null && Token.IsCancelRequested) {
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
#if NET48
|
||||
proc.Kill();
|
||||
#else
|
||||
proc.Kill(true);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Data == null) return;
|
||||
OnReadline(e.Data);
|
||||
};
|
||||
proc.ErrorDataReceived += (o, e) => {
|
||||
if (Token != null && Token.IsCancelRequested) {
|
||||
isCancelled = true;
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
#if NET48
|
||||
proc.Kill();
|
||||
#else
|
||||
proc.Kill(true);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Data == null) return;
|
||||
if (TraitErrorAsOutput) OnReadline(e.Data);
|
||||
|
||||
if (string.IsNullOrEmpty(e.Data)) return;
|
||||
if (progressFilter.IsMatch(e.Data)) return;
|
||||
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return;
|
||||
errs.Add(e.Data);
|
||||
};
|
||||
|
||||
proc.Start();
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
proc.WaitForExit();
|
||||
|
||||
int exitCode = proc.ExitCode;
|
||||
proc.Close();
|
||||
|
||||
if (!isCancelled && exitCode != 0 && errs.Count > 0) {
|
||||
Models.Exception.Raise(string.Join("\n", errs));
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接读取全部标准输出
|
||||
/// </summary>
|
||||
public ReadToEndResult ReadToEnd() {
|
||||
var start = new ProcessStartInfo();
|
||||
start.FileName = Models.Preference.Instance.Git.Path;
|
||||
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.RedirectStandardOutput = true;
|
||||
start.RedirectStandardError = true;
|
||||
start.StandardOutputEncoding = Encoding.UTF8;
|
||||
start.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
|
||||
|
||||
var proc = new Process() { StartInfo = start };
|
||||
proc.Start();
|
||||
|
||||
var rs = new ReadToEndResult();
|
||||
rs.Output = proc.StandardOutput.ReadToEnd();
|
||||
rs.Error = proc.StandardError.ReadToEnd();
|
||||
|
||||
proc.WaitForExit();
|
||||
rs.IsSuccess = proc.ExitCode == 0;
|
||||
proc.Close();
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用Exec时的读取函数
|
||||
/// </summary>
|
||||
/// <param name="line"></param>
|
||||
public virtual void OnReadline(string line) {}
|
||||
}
|
||||
}
|
19
src/Commands/Commit.cs
Normal file
19
src/Commands/Commit.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// `git commit`命令
|
||||
/// </summary>
|
||||
public class Commit : Command {
|
||||
private string msg = null;
|
||||
|
||||
public Commit(string repo, string message, bool amend) {
|
||||
msg = Path.GetTempFileName();
|
||||
File.WriteAllText(msg, message);
|
||||
|
||||
Cwd = repo;
|
||||
Args = $"commit --file=\"{msg}\"";
|
||||
if (amend) Args += " --amend --no-edit";
|
||||
}
|
||||
}
|
||||
}
|
39
src/Commands/CommitChanges.cs
Normal file
39
src/Commands/CommitChanges.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取得一个提交的变更列表
|
||||
/// </summary>
|
||||
public class CommitChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public CommitChanges(string cwd, string commit) {
|
||||
Cwd = cwd;
|
||||
Args = $"show --name-status {commit}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
|
||||
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
|
||||
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
|
||||
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
|
||||
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
src/Commands/CommitRangeChanges.cs
Normal file
39
src/Commands/CommitRangeChanges.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 对比两个提交间的变更
|
||||
/// </summary>
|
||||
public class CommitRangeChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public CommitRangeChanges(string cwd, string start, string end) {
|
||||
Cwd = cwd;
|
||||
Args = $"diff --name-status {start} {end}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
|
||||
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
|
||||
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
|
||||
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
|
||||
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
140
src/Commands/Commits.cs
Normal file
140
src/Commands/Commits.cs
Normal file
|
@ -0,0 +1,140 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取得提交列表
|
||||
/// </summary>
|
||||
public class Commits : Command {
|
||||
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
|
||||
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
|
||||
|
||||
private List<Models.Commit> commits = new List<Models.Commit>();
|
||||
private Models.Commit current = null;
|
||||
private bool isSkipingGpgsig = false;
|
||||
private bool isHeadFounded = false;
|
||||
private bool findFirstMerged = true;
|
||||
|
||||
public Commits(string path, string limits, bool needFindHead = true) {
|
||||
Cwd = path;
|
||||
Args = "log --date-order --decorate=full --pretty=raw " + limits;
|
||||
findFirstMerged = needFindHead;
|
||||
}
|
||||
|
||||
public List<Models.Commit> Result() {
|
||||
Exec();
|
||||
|
||||
if (current != null) {
|
||||
current.Message = current.Message.Trim();
|
||||
commits.Add(current);
|
||||
}
|
||||
|
||||
if (findFirstMerged && !isHeadFounded && commits.Count > 0) {
|
||||
MarkFirstMerged();
|
||||
}
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (isSkipingGpgsig) {
|
||||
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false;
|
||||
return;
|
||||
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) {
|
||||
isSkipingGpgsig = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
|
||||
if (current != null) {
|
||||
current.Message = current.Message.Trim();
|
||||
commits.Add(current);
|
||||
}
|
||||
|
||||
current = new Models.Commit();
|
||||
line = line.Substring(7);
|
||||
|
||||
var decoratorStart = line.IndexOf('(');
|
||||
if (decoratorStart < 0) {
|
||||
current.SHA = line.Trim();
|
||||
} else {
|
||||
current.SHA = line.Substring(0, decoratorStart).Trim();
|
||||
current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1));
|
||||
if (!isHeadFounded) isHeadFounded = current.IsMerged;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (current == null) return;
|
||||
|
||||
if (line.StartsWith("tree ", StringComparison.Ordinal)) {
|
||||
return;
|
||||
} else if (line.StartsWith("parent ", StringComparison.Ordinal)) {
|
||||
current.Parents.Add(line.Substring("parent ".Length));
|
||||
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
|
||||
current.Author.Parse(line);
|
||||
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) {
|
||||
current.Committer.Parse(line);
|
||||
} else if (string.IsNullOrEmpty(current.Subject)) {
|
||||
current.Subject = line.Trim();
|
||||
} else {
|
||||
current.Message += (line.Trim() + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParseDecorators(List<Models.Decorator> decorators, string data) {
|
||||
bool isHeadOfCurrent = false;
|
||||
|
||||
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var sub in subs) {
|
||||
var d = sub.Trim();
|
||||
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) {
|
||||
decorators.Add(new Models.Decorator() {
|
||||
Type = Models.DecoratorType.Tag,
|
||||
Name = d.Substring(15).Trim(),
|
||||
});
|
||||
} else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) {
|
||||
continue;
|
||||
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) {
|
||||
isHeadOfCurrent = true;
|
||||
decorators.Add(new Models.Decorator() {
|
||||
Type = Models.DecoratorType.CurrentBranchHead,
|
||||
Name = d.Substring(19).Trim(),
|
||||
});
|
||||
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) {
|
||||
decorators.Add(new Models.Decorator() {
|
||||
Type = Models.DecoratorType.LocalBranchHead,
|
||||
Name = d.Substring(11).Trim(),
|
||||
});
|
||||
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) {
|
||||
decorators.Add(new Models.Decorator() {
|
||||
Type = Models.DecoratorType.RemoteBranchHead,
|
||||
Name = d.Substring(13).Trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return isHeadOfCurrent;
|
||||
}
|
||||
|
||||
private void MarkFirstMerged() {
|
||||
Args = $"log --since=\"{commits.Last().Committer.Time}\" --min-parents=2 --format=\"%H\"";
|
||||
|
||||
var rs = ReadToEnd();
|
||||
var shas = rs.Output.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (shas.Length == 0) return;
|
||||
|
||||
var merges = commits.Where(x => x.Parents.Count > 1).ToList();
|
||||
foreach (var sha in shas) {
|
||||
var c = merges.Find(x => x.SHA == sha);
|
||||
if (c != null) {
|
||||
c.IsMerged = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
src/Commands/Config.cs
Normal file
37
src/Commands/Config.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// config命令
|
||||
/// </summary>
|
||||
public class Config : Command {
|
||||
|
||||
public Config() {
|
||||
}
|
||||
|
||||
public Config(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public string Get(string key) {
|
||||
Args = $"config {key}";
|
||||
return ReadToEnd().Output.Trim();
|
||||
}
|
||||
|
||||
public bool Set(string key, string val, bool allowEmpty = false) {
|
||||
if (!allowEmpty && string.IsNullOrEmpty(val)) {
|
||||
if (string.IsNullOrEmpty(Cwd)) {
|
||||
Args = $"config --global --unset {key}";
|
||||
} else {
|
||||
Args = $"config --unset {key}";
|
||||
}
|
||||
} else {
|
||||
if (string.IsNullOrEmpty(Cwd)) {
|
||||
Args = $"config --global {key} \"{val}\"";
|
||||
} else {
|
||||
Args = $"config {key} \"{val}\"";
|
||||
}
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
61
src/Commands/Diff.cs
Normal file
61
src/Commands/Diff.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// Diff命令(用于文件文件比对)
|
||||
/// </summary>
|
||||
public class Diff : Command {
|
||||
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
|
||||
private Models.TextChanges changes = new Models.TextChanges();
|
||||
private int oldLine = 0;
|
||||
private int newLine = 0;
|
||||
|
||||
public Diff(string repo, string args) {
|
||||
Cwd = repo;
|
||||
Args = $"diff --ignore-cr-at-eol {args}";
|
||||
}
|
||||
|
||||
public Models.TextChanges Result() {
|
||||
Exec();
|
||||
if (changes.IsBinary) changes.Lines.Clear();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (changes.IsBinary) return;
|
||||
|
||||
if (changes.Lines.Count == 0) {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (!match.Success) {
|
||||
if (line.StartsWith("Binary", StringComparison.Ordinal)) changes.IsBinary = true;
|
||||
return;
|
||||
}
|
||||
|
||||
oldLine = int.Parse(match.Groups[1].Value);
|
||||
newLine = int.Parse(match.Groups[2].Value);
|
||||
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", ""));
|
||||
} else {
|
||||
var ch = line[0];
|
||||
if (ch == '-') {
|
||||
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", ""));
|
||||
oldLine++;
|
||||
} else if (ch == '+') {
|
||||
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}"));
|
||||
newLine++;
|
||||
} else if (ch != '\\') {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (match.Success) {
|
||||
oldLine = int.Parse(match.Groups[1].Value);
|
||||
newLine = int.Parse(match.Groups[2].Value);
|
||||
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", ""));
|
||||
} else {
|
||||
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}"));
|
||||
oldLine++;
|
||||
newLine++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
src/Commands/Discard.cs
Normal file
37
src/Commands/Discard.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 忽略变更
|
||||
/// </summary>
|
||||
public class Discard {
|
||||
private string repo = null;
|
||||
private List<string> files = new List<string>();
|
||||
|
||||
public Discard(string repo, List<Models.Change> changes) {
|
||||
this.repo = repo;
|
||||
|
||||
if (changes != null && changes.Count > 0) {
|
||||
foreach (var c in changes) {
|
||||
if (c.WorkTree == Models.Change.Status.Untracked || c.WorkTree == Models.Change.Status.Added) continue;
|
||||
files.Add(c.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Exec() {
|
||||
if (files.Count == 0) {
|
||||
new Reset(repo, "HEAD", "--hard").Exec();
|
||||
} else {
|
||||
for (int i = 0; i < files.Count; i += 10) {
|
||||
var count = Math.Min(10, files.Count - i);
|
||||
new Checkout(repo).Files(files.GetRange(i, count));
|
||||
}
|
||||
}
|
||||
|
||||
new Clean(repo).Exec();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
77
src/Commands/Fetch.cs
Normal file
77
src/Commands/Fetch.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 拉取
|
||||
/// </summary>
|
||||
public class Fetch : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler) {
|
||||
Cwd = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
Args = "-c credential.helper=manager fetch --progress --verbose ";
|
||||
if (prune) Args += "--prune ";
|
||||
Args += remote;
|
||||
handler = outputHandler;
|
||||
AutoFetch.MarkFetched(repo);
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动拉取(每隔10分钟)
|
||||
/// </summary>
|
||||
public class AutoFetch {
|
||||
private static Dictionary<string, AutoFetch> jobs = new Dictionary<string, AutoFetch>();
|
||||
|
||||
private Fetch cmd = null;
|
||||
private long nextFetchPoint = 0;
|
||||
private Timer timer = null;
|
||||
|
||||
public static void Start(string repo) {
|
||||
if (!Models.Preference.Instance.General.AutoFetchRemotes) return;
|
||||
|
||||
// 只自动更新加入管理列表中的仓库(子模块等不自动更新)
|
||||
var exists = Models.Preference.Instance.FindRepository(repo);
|
||||
if (exists == null) return;
|
||||
|
||||
var job = new AutoFetch(repo);
|
||||
jobs.Add(repo, job);
|
||||
}
|
||||
|
||||
public static void MarkFetched(string repo) {
|
||||
if (!jobs.ContainsKey(repo)) return;
|
||||
jobs[repo].nextFetchPoint = DateTime.Now.AddMinutes(1).ToFileTime();
|
||||
}
|
||||
|
||||
public static void Stop(string repo) {
|
||||
if (!jobs.ContainsKey(repo)) return;
|
||||
|
||||
jobs[repo].timer.Dispose();
|
||||
jobs.Remove(repo);
|
||||
}
|
||||
|
||||
public AutoFetch(string repo) {
|
||||
cmd = new Fetch(repo, "--all", true, null);
|
||||
nextFetchPoint = DateTime.Now.AddMinutes(1).ToFileTime();
|
||||
timer = new Timer(OnTick, null, 60000, 10000);
|
||||
}
|
||||
|
||||
private void OnTick(object o) {
|
||||
var now = DateTime.Now.ToFileTime();
|
||||
if (nextFetchPoint > now) return;
|
||||
|
||||
Models.Watcher.SetEnabled(cmd.Cwd, false);
|
||||
cmd.Exec();
|
||||
nextFetchPoint = DateTime.Now.AddMinutes(1).ToFileTime();
|
||||
Models.Watcher.SetEnabled(cmd.Cwd, true);
|
||||
}
|
||||
}
|
||||
}
|
12
src/Commands/FormatPatch.cs
Normal file
12
src/Commands/FormatPatch.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 将Commit另存为Patch文件
|
||||
/// </summary>
|
||||
public class FormatPatch : Command {
|
||||
|
||||
public FormatPatch(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"format-patch {commit} -1 -o \"{path}\"";
|
||||
}
|
||||
}
|
||||
}
|
17
src/Commands/GetRepositoryRootPath.cs
Normal file
17
src/Commands/GetRepositoryRootPath.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得一个库的根路径
|
||||
/// </summary>
|
||||
public class GetRepositoryRootPath : Command {
|
||||
public GetRepositoryRootPath(string path) {
|
||||
Cwd = path;
|
||||
Args = "rev-parse --show-toplevel";
|
||||
}
|
||||
|
||||
public string Result() {
|
||||
var rs = ReadToEnd().Output;
|
||||
if (string.IsNullOrEmpty(rs)) return null;
|
||||
return rs.Trim();
|
||||
}
|
||||
}
|
||||
}
|
71
src/Commands/GitFlow.cs
Normal file
71
src/Commands/GitFlow.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// Git-Flow命令
|
||||
/// </summary>
|
||||
public class GitFlow : Command {
|
||||
|
||||
public GitFlow(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Init(string master, string develop, string feature, string release, string hotfix, string version) {
|
||||
var branches = new Branches(Cwd).Result();
|
||||
var current = branches.Find(x => x.IsCurrent);
|
||||
|
||||
var masterBranch = branches.Find(x => x.Name == master);
|
||||
if (masterBranch == null) new Branch(Cwd, master).Create(current.Head);
|
||||
|
||||
var devBranch = branches.Find(x => x.Name == develop);
|
||||
if (devBranch == null) new Branch(Cwd, develop).Create(current.Head);
|
||||
|
||||
var cmd = new Config(Cwd);
|
||||
cmd.Set("gitflow.branch.master", master);
|
||||
cmd.Set("gitflow.branch.develop", develop);
|
||||
cmd.Set("gitflow.prefix.feature", feature);
|
||||
cmd.Set("gitflow.prefix.bugfix", "bugfix/");
|
||||
cmd.Set("gitflow.prefix.release", release);
|
||||
cmd.Set("gitflow.prefix.hotfix", hotfix);
|
||||
cmd.Set("gitflow.prefix.support", "support/");
|
||||
cmd.Set("gitflow.prefix.versiontag", version, true);
|
||||
|
||||
Args = "flow init -d";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public void Start(Models.GitFlowBranchType type, string name) {
|
||||
switch (type) {
|
||||
case Models.GitFlowBranchType.Feature:
|
||||
Args = $"flow feature start {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Release:
|
||||
Args = $"flow release start {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Hotfix:
|
||||
Args = $"flow hotfix start {name}";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Exec();
|
||||
}
|
||||
|
||||
public void Finish(Models.GitFlowBranchType type, string name) {
|
||||
switch (type) {
|
||||
case Models.GitFlowBranchType.Feature:
|
||||
Args = $"flow feature finish {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Release:
|
||||
Args = $"flow release finish {name}";
|
||||
break;
|
||||
case Models.GitFlowBranchType.Hotfix:
|
||||
Args = $"flow hotfix finish {name}";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Exec();
|
||||
}
|
||||
}
|
||||
}
|
13
src/Commands/Init.cs
Normal file
13
src/Commands/Init.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 初始化Git仓库
|
||||
/// </summary>
|
||||
public class Init : Command {
|
||||
|
||||
public Init(string workDir) {
|
||||
Cwd = workDir;
|
||||
Args = "init -q";
|
||||
}
|
||||
}
|
||||
}
|
18
src/Commands/IsBinaryFile.cs
Normal file
18
src/Commands/IsBinaryFile.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查询指定版本下的某文件是否是二进制文件
|
||||
/// </summary>
|
||||
public class IsBinaryFile : Command {
|
||||
private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$");
|
||||
public IsBinaryFile(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
|
||||
}
|
||||
|
||||
public bool Result() {
|
||||
return REG_TEST.IsMatch(ReadToEnd().Output);
|
||||
}
|
||||
}
|
||||
}
|
16
src/Commands/IsLFSFiltered.cs
Normal file
16
src/Commands/IsLFSFiltered.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 检测目录是否被LFS管理
|
||||
/// </summary>
|
||||
public class IsLFSFiltered : Command {
|
||||
public IsLFSFiltered(string cwd, string path) {
|
||||
Cwd = cwd;
|
||||
Args = $"check-attr -a -z \"{path}\"";
|
||||
}
|
||||
|
||||
public bool Result() {
|
||||
var rs = ReadToEnd();
|
||||
return rs.Output.Contains("filter\0lfs");
|
||||
}
|
||||
}
|
||||
}
|
64
src/Commands/LocalChanges.cs
Normal file
64
src/Commands/LocalChanges.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得本地工作副本变更
|
||||
/// </summary>
|
||||
public class LocalChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public LocalChanges(string path) {
|
||||
Cwd = path;
|
||||
Args = "status -uall --ignore-submodules=dirty --porcelain";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status) {
|
||||
case " M": change.Set(Models.Change.Status.None, Models.Change.Status.Modified); break;
|
||||
case " A": change.Set(Models.Change.Status.None, Models.Change.Status.Added); break;
|
||||
case " D": change.Set(Models.Change.Status.None, Models.Change.Status.Deleted); break;
|
||||
case " R": change.Set(Models.Change.Status.None, Models.Change.Status.Renamed); break;
|
||||
case " C": change.Set(Models.Change.Status.None, Models.Change.Status.Copied); break;
|
||||
case "M": change.Set(Models.Change.Status.Modified, Models.Change.Status.None); break;
|
||||
case "MM": change.Set(Models.Change.Status.Modified, Models.Change.Status.Modified); break;
|
||||
case "MD": change.Set(Models.Change.Status.Modified, Models.Change.Status.Deleted); break;
|
||||
case "A": change.Set(Models.Change.Status.Added, Models.Change.Status.None); break;
|
||||
case "AM": change.Set(Models.Change.Status.Added, Models.Change.Status.Modified); break;
|
||||
case "AD": change.Set(Models.Change.Status.Added, Models.Change.Status.Deleted); break;
|
||||
case "D": change.Set(Models.Change.Status.Deleted, Models.Change.Status.None); break;
|
||||
case "R": change.Set(Models.Change.Status.Renamed, Models.Change.Status.None); break;
|
||||
case "RM": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Modified); break;
|
||||
case "RD": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Deleted); break;
|
||||
case "C": change.Set(Models.Change.Status.Copied, Models.Change.Status.None); break;
|
||||
case "CM": change.Set(Models.Change.Status.Copied, Models.Change.Status.Modified); break;
|
||||
case "CD": change.Set(Models.Change.Status.Copied, Models.Change.Status.Deleted); break;
|
||||
case "DR": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Renamed); break;
|
||||
case "DC": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Copied); break;
|
||||
case "DD": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Deleted); break;
|
||||
case "AU": change.Set(Models.Change.Status.Added, Models.Change.Status.Unmerged); break;
|
||||
case "UD": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Deleted); break;
|
||||
case "UA": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Added); break;
|
||||
case "DU": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Unmerged); break;
|
||||
case "AA": change.Set(Models.Change.Status.Added, Models.Change.Status.Added); break;
|
||||
case "UU": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Unmerged); break;
|
||||
case "??": change.Set(Models.Change.Status.Untracked, Models.Change.Status.Untracked); break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
changes.Add(change);
|
||||
}
|
||||
}
|
||||
}
|
12
src/Commands/Merge.cs
Normal file
12
src/Commands/Merge.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 合并分支
|
||||
/// </summary>
|
||||
public class Merge : Command {
|
||||
|
||||
public Merge(string repo, string source, string mode) {
|
||||
Cwd = repo;
|
||||
Args = $"merge {source} {mode}";
|
||||
}
|
||||
}
|
||||
}
|
48
src/Commands/Pull.cs
Normal file
48
src/Commands/Pull.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 拉回
|
||||
/// </summary>
|
||||
public class Pull : Command {
|
||||
private Action<string> handler = null;
|
||||
private bool needStash = false;
|
||||
|
||||
public Pull(string repo, string remote, string branch, bool useRebase, bool autoStash, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
Args = "-c credential.helper=manager pull --verbose --progress --tags ";
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
|
||||
if (useRebase) Args += "--rebase ";
|
||||
if (autoStash) {
|
||||
if (useRebase) Args += "--autostash ";
|
||||
else needStash = true;
|
||||
}
|
||||
|
||||
Args += $"{remote} {branch}";
|
||||
}
|
||||
|
||||
public bool Run() {
|
||||
if (needStash) {
|
||||
var changes = new LocalChanges(Cwd).Result();
|
||||
if (changes.Count > 0) {
|
||||
if (!new Stash(Cwd).Push(null, "PULL_AUTO_STASH", true)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
needStash = false;
|
||||
}
|
||||
}
|
||||
|
||||
var succ = Exec();
|
||||
if (needStash) new Stash(Cwd).Pop("stash@{0}");
|
||||
return succ;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
}
|
||||
}
|
39
src/Commands/Push.cs
Normal file
39
src/Commands/Push.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 推送
|
||||
/// </summary>
|
||||
public class Push : Command {
|
||||
private Action<string> handler = null;
|
||||
|
||||
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress) {
|
||||
Cwd = repo;
|
||||
TraitErrorAsOutput = true;
|
||||
handler = onProgress;
|
||||
Args = "-c credential.helper=manager push --progress --verbose ";
|
||||
|
||||
if (withTags) Args += "--tags ";
|
||||
if (track) Args += "-u ";
|
||||
if (force) Args += "--force-with-lease ";
|
||||
|
||||
Args += $"{remote} {local}:{remoteBranch}";
|
||||
}
|
||||
|
||||
public Push(string repo, string remote, string branch) {
|
||||
Cwd = repo;
|
||||
Args = $"-c credential.helper=manager push {remote} --delete {branch}";
|
||||
}
|
||||
|
||||
public Push(string repo, string remote, string tag, bool isDelete) {
|
||||
Cwd = repo;
|
||||
Args = $"-c credential.helper=manager push ";
|
||||
if (isDelete) Args += "--delete ";
|
||||
Args += $"{remote} refs/tags/{tag}";
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
handler?.Invoke(line);
|
||||
}
|
||||
}
|
||||
}
|
26
src/Commands/QueryFileContent.cs
Normal file
26
src/Commands/QueryFileContent.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得指定提交下的某文件内容
|
||||
/// </summary>
|
||||
public class QueryFileContent : Command {
|
||||
private List<Models.TextLine> lines = new List<Models.TextLine>();
|
||||
private int added = 0;
|
||||
|
||||
public QueryFileContent(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"show {commit}:\"{path}\"";
|
||||
}
|
||||
|
||||
public List<Models.TextLine> Result() {
|
||||
Exec();
|
||||
return lines;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
added++;
|
||||
lines.Add(new Models.TextLine() { Number = added, Data = line });
|
||||
}
|
||||
}
|
||||
}
|
50
src/Commands/QueryFileSizeChange.cs
Normal file
50
src/Commands/QueryFileSizeChange.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查询文件大小变化
|
||||
/// </summary>
|
||||
public class QueryFileSizeChange {
|
||||
|
||||
class QuerySizeCmd : Command {
|
||||
public QuerySizeCmd(string repo, string path, string revision) {
|
||||
Cwd = repo;
|
||||
Args = $"cat-file -s {revision}:\"{path}\"";
|
||||
}
|
||||
|
||||
public long Result() {
|
||||
string data = ReadToEnd().Output;
|
||||
long size;
|
||||
if (!long.TryParse(data, out size)) size = 0;
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
private Models.FileSizeChange change = new Models.FileSizeChange();
|
||||
|
||||
public QueryFileSizeChange(string repo, string[] revisions, string path, string orgPath) {
|
||||
if (revisions.Length == 0) {
|
||||
change.NewSize = new FileInfo(Path.Combine(repo, path)).Length;
|
||||
change.OldSize = new QuerySizeCmd(repo, path, "HEAD").Result();
|
||||
} else if (revisions.Length == 1) {
|
||||
change.NewSize = new QuerySizeCmd(repo, path, "HEAD").Result();
|
||||
if (string.IsNullOrEmpty(orgPath)) {
|
||||
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
|
||||
} else {
|
||||
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
|
||||
}
|
||||
} else {
|
||||
change.NewSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
|
||||
if (string.IsNullOrEmpty(orgPath)) {
|
||||
change.OldSize = new QuerySizeCmd(repo, path, revisions[1]).Result();
|
||||
} else {
|
||||
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[1]).Result();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Models.FileSizeChange Result() {
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
23
src/Commands/QueryGitDir.cs
Normal file
23
src/Commands/QueryGitDir.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 取得GitDir
|
||||
/// </summary>
|
||||
public class QueryGitDir : Command {
|
||||
public QueryGitDir(string workDir) {
|
||||
Cwd = workDir;
|
||||
Args = "rev-parse --git-dir";
|
||||
}
|
||||
|
||||
public string Result() {
|
||||
var rs = ReadToEnd().Output;
|
||||
if (string.IsNullOrEmpty(rs)) return null;
|
||||
|
||||
rs = rs.Trim();
|
||||
if (Path.IsPathRooted(rs)) return rs;
|
||||
return Path.Combine(Cwd, rs);
|
||||
}
|
||||
}
|
||||
}
|
28
src/Commands/QueryLFSObject.cs
Normal file
28
src/Commands/QueryLFSObject.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取得一个LFS对象的信息
|
||||
/// </summary>
|
||||
public class QueryLFSObject : Command {
|
||||
private Models.LFSObject obj = new Models.LFSObject();
|
||||
|
||||
public QueryLFSObject(string repo, string commit, string path) {
|
||||
Cwd = repo;
|
||||
Args = $"show {commit}:\"{path}\"";
|
||||
}
|
||||
|
||||
public Models.LFSObject Result() {
|
||||
Exec();
|
||||
return obj;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) {
|
||||
obj.OID = line.Substring(11).Trim();
|
||||
} else if (line.StartsWith("size")) {
|
||||
obj.Size = int.Parse(line.Substring(4).Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
src/Commands/QueryLFSObjectChange.cs
Normal file
41
src/Commands/QueryLFSObjectChange.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查询LFS对象变更
|
||||
/// </summary>
|
||||
public class QueryLFSObjectChange : Command {
|
||||
private Models.LFSChange change = new Models.LFSChange();
|
||||
|
||||
public QueryLFSObjectChange(string repo, string args) {
|
||||
Cwd = repo;
|
||||
Args = $"diff --ignore-cr-at-eol {args}";
|
||||
}
|
||||
|
||||
public Models.LFSChange Result() {
|
||||
Exec();
|
||||
return change;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var ch = line[0];
|
||||
if (ch == '-') {
|
||||
if (change.Old == null) change.Old = new Models.LFSObject();
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
change.Old.OID = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
change.Old.Size = int.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (ch == '+') {
|
||||
if (change.New == null) change.New = new Models.LFSObject();
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
change.New.OID = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
change.New.Size = int.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (line.StartsWith(" size ")) {
|
||||
change.New.Size = change.Old.Size = int.Parse(line.Substring(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/Commands/Rebase.cs
Normal file
14
src/Commands/Rebase.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 变基命令
|
||||
/// </summary>
|
||||
public class Rebase : Command {
|
||||
|
||||
public Rebase(string repo, string basedOn, bool autoStash) {
|
||||
Cwd = repo;
|
||||
Args = "rebase ";
|
||||
if (autoStash) Args += "--autostash ";
|
||||
Args += basedOn;
|
||||
}
|
||||
}
|
||||
}
|
31
src/Commands/Remote.cs
Normal file
31
src/Commands/Remote.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 远程操作
|
||||
/// </summary>
|
||||
public class Remote : Command {
|
||||
|
||||
public Remote(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Add(string name, string url) {
|
||||
Args = $"remote add {name} {url}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string name) {
|
||||
Args = $"remote remove {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Rename(string name, string to) {
|
||||
Args = $"remote rename {name} {to}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool SetURL(string name, string url) {
|
||||
Args = $"remote set-url {name} {url}";
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
35
src/Commands/Remotes.cs
Normal file
35
src/Commands/Remotes.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 获取远程列表
|
||||
/// </summary>
|
||||
public class Remotes : Command {
|
||||
private static readonly Regex REG_REMOTE = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
|
||||
private List<Models.Remote> loaded = new List<Models.Remote>();
|
||||
|
||||
public Remotes(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "remote -v";
|
||||
}
|
||||
|
||||
public List<Models.Remote> Result() {
|
||||
Exec();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_REMOTE.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var remote = new Models.Remote() {
|
||||
Name = match.Groups[1].Value,
|
||||
URL = match.Groups[2].Value,
|
||||
};
|
||||
|
||||
if (loaded.Find(x => x.Name == remote.Name) != null) return;
|
||||
loaded.Add(remote);
|
||||
}
|
||||
}
|
||||
}
|
33
src/Commands/Reset.cs
Normal file
33
src/Commands/Reset.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 重置命令
|
||||
/// </summary>
|
||||
public class Reset : Command {
|
||||
|
||||
public Reset(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "reset";
|
||||
}
|
||||
|
||||
public Reset(string repo, string revision, string mode) {
|
||||
Cwd = repo;
|
||||
Args = $"reset {mode} {revision}";
|
||||
}
|
||||
|
||||
public Reset(string repo, List<string> files) {
|
||||
Cwd = repo;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("reset --");
|
||||
foreach (var f in files) {
|
||||
builder.Append(" \"");
|
||||
builder.Append(f);
|
||||
builder.Append("\"");
|
||||
}
|
||||
Args = builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
13
src/Commands/Revert.cs
Normal file
13
src/Commands/Revert.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 撤销提交
|
||||
/// </summary>
|
||||
public class Revert : Command {
|
||||
|
||||
public Revert(string repo, string commit, bool autoCommit) {
|
||||
Cwd = repo;
|
||||
Args = $"revert {commit} --no-edit";
|
||||
if (!autoCommit) Args += " --no-commit";
|
||||
}
|
||||
}
|
||||
}
|
41
src/Commands/RevisionObjects.cs
Normal file
41
src/Commands/RevisionObjects.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 取出指定Revision下的文件列表
|
||||
/// </summary>
|
||||
public class RevisionObjects : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
|
||||
private List<Models.Object> objects = new List<Models.Object>();
|
||||
|
||||
public RevisionObjects(string cwd, string sha) {
|
||||
Cwd = cwd;
|
||||
Args = $"ls-tree -r {sha}";
|
||||
}
|
||||
|
||||
public List<Models.Object> Result() {
|
||||
Exec();
|
||||
return objects;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var obj = new Models.Object();
|
||||
obj.SHA = match.Groups[2].Value;
|
||||
obj.Type = Models.ObjectType.Blob;
|
||||
obj.Path = match.Groups[3].Value;
|
||||
|
||||
switch (match.Groups[1].Value) {
|
||||
case "blob": obj.Type = Models.ObjectType.Blob; break;
|
||||
case "tree": obj.Type = Models.ObjectType.Tree; break;
|
||||
case "tag": obj.Type = Models.ObjectType.Tag; break;
|
||||
case "commit": obj.Type = Models.ObjectType.Commit; break;
|
||||
}
|
||||
|
||||
objects.Add(obj);
|
||||
}
|
||||
}
|
||||
}
|
26
src/Commands/SaveChangesToPatch.cs
Normal file
26
src/Commands/SaveChangesToPatch.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 将Changes保存到文件流中
|
||||
/// </summary>
|
||||
public class SaveChangeToStream : Command {
|
||||
private StreamWriter writer = null;
|
||||
|
||||
public SaveChangeToStream(string repo, Models.Change change, StreamWriter to) {
|
||||
Cwd = repo;
|
||||
if (change.WorkTree == Models.Change.Status.Added || change.WorkTree == Models.Change.Status.Untracked) {
|
||||
Args = $"diff --no-index --no-ext-diff --find-renames -- /dev/null \"{change.Path}\"";
|
||||
} else {
|
||||
var pathspec = $"\"{change.Path}\"";
|
||||
if (!string.IsNullOrEmpty(change.OriginalPath)) pathspec = $"\"{change.OriginalPath}\" \"{change.Path}\"";
|
||||
Args = $"diff --binary --no-ext-diff --find-renames --full-index -- {pathspec}";
|
||||
}
|
||||
writer = to;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
44
src/Commands/SaveRevisionFile.cs
Normal file
44
src/Commands/SaveRevisionFile.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 保存指定版本的文件
|
||||
/// </summary>
|
||||
public class SaveRevisionFile {
|
||||
private string cwd = "";
|
||||
private string bat = "";
|
||||
|
||||
public SaveRevisionFile(string repo, string path, string sha, string saveTo) {
|
||||
var tmp = Path.GetTempFileName();
|
||||
var cmd = $"\"{Models.Preference.Instance.Git.Path}\" --no-pager ";
|
||||
|
||||
var isLFS = new IsLFSFiltered(repo, path).Result();
|
||||
if (isLFS) {
|
||||
cmd += $"show {sha}:\"{path}\" > {tmp}.lfs\n";
|
||||
cmd += $"\"{Models.Preference.Instance.Git.Path}\" --no-pager lfs smudge < {tmp}.lfs > \"{saveTo}\"\n";
|
||||
} else {
|
||||
cmd += $"show {sha}:\"{path}\" > \"{saveTo}\"\n";
|
||||
}
|
||||
|
||||
cwd = repo;
|
||||
bat = tmp + ".bat";
|
||||
|
||||
File.WriteAllText(bat, cmd);
|
||||
}
|
||||
|
||||
public void Exec() {
|
||||
var starter = new ProcessStartInfo();
|
||||
starter.FileName = bat;
|
||||
starter.WorkingDirectory = cwd;
|
||||
starter.CreateNoWindow = true;
|
||||
starter.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
|
||||
var proc = Process.Start(starter);
|
||||
proc.WaitForExit();
|
||||
proc.Close();
|
||||
|
||||
File.Delete(bat);
|
||||
}
|
||||
}
|
||||
}
|
50
src/Commands/Stash.cs
Normal file
50
src/Commands/Stash.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 单个贮藏相关操作
|
||||
/// </summary>
|
||||
public class Stash : Command {
|
||||
|
||||
public Stash(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Push(List<string> files, string message, bool includeUntracked) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("stash push ");
|
||||
if (includeUntracked) builder.Append("-u ");
|
||||
builder.Append("-m \"");
|
||||
builder.Append(message);
|
||||
builder.Append("\" ");
|
||||
|
||||
if (files != null && files.Count > 0) {
|
||||
builder.Append("--");
|
||||
foreach (var f in files) {
|
||||
builder.Append(" \"");
|
||||
builder.Append(f);
|
||||
builder.Append("\"");
|
||||
}
|
||||
}
|
||||
|
||||
Args = builder.ToString();
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Apply(string name) {
|
||||
Args = $"stash apply -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Pop(string name) {
|
||||
Args = $"stash pop -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Drop(string name) {
|
||||
Args = $"stash drop -q {name}";
|
||||
return Exec();
|
||||
}
|
||||
}
|
||||
}
|
38
src/Commands/StashChanges.cs
Normal file
38
src/Commands/StashChanges.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 查看Stash中的修改
|
||||
/// </summary>
|
||||
public class StashChanges : Command {
|
||||
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
private List<Models.Change> changes = new List<Models.Change>();
|
||||
|
||||
public StashChanges(string repo, string sha) {
|
||||
Cwd = repo;
|
||||
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
|
||||
}
|
||||
|
||||
public List<Models.Change> Result() {
|
||||
Exec();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var change = new Models.Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
|
||||
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
|
||||
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
|
||||
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
|
||||
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
src/Commands/Stashes.cs
Normal file
44
src/Commands/Stashes.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 解析当前仓库中的贮藏
|
||||
/// </summary>
|
||||
public class Stashes : Command {
|
||||
private static readonly Regex REG_STASH = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$");
|
||||
private List<Models.Stash> parsed = new List<Models.Stash>();
|
||||
private Models.Stash current = null;
|
||||
|
||||
public Stashes(string path) {
|
||||
Cwd = path;
|
||||
Args = "stash list --pretty=raw";
|
||||
}
|
||||
|
||||
public List<Models.Stash> Result() {
|
||||
Exec();
|
||||
if (current != null) parsed.Add(current);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
|
||||
if (current != null && !string.IsNullOrEmpty(current.Name)) parsed.Add(current);
|
||||
current = new Models.Stash() { SHA = line.Substring(7, 8) };
|
||||
return;
|
||||
}
|
||||
|
||||
if (current == null) return;
|
||||
|
||||
if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) {
|
||||
var match = REG_STASH.Match(line);
|
||||
if (match.Success) current.Name = match.Groups[1].Value;
|
||||
} else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal)) {
|
||||
current.Message = line.Substring(16);
|
||||
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
|
||||
current.Author.Parse(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
src/Commands/Submodule.cs
Normal file
44
src/Commands/Submodule.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 子模块
|
||||
/// </summary>
|
||||
public class Submodule : Command {
|
||||
private Action<string> onProgress = null;
|
||||
|
||||
public Submodule(string cwd) {
|
||||
Cwd = cwd;
|
||||
}
|
||||
|
||||
public bool Add(string url, string path, bool recursive, Action<string> handler) {
|
||||
Args = $"submodule add {url} {path}";
|
||||
onProgress = handler;
|
||||
if (!Exec()) return false;
|
||||
|
||||
if (recursive) {
|
||||
Args = $"submodule update --init --recursive -- {path}";
|
||||
return Exec();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Update() {
|
||||
Args = $"submodule update --rebase --remote";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string path) {
|
||||
Args = $"submodule deinit -f {path}";
|
||||
if (!Exec()) return false;
|
||||
|
||||
Args = $"rm -rf {path}";
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
onProgress?.Invoke(line);
|
||||
}
|
||||
}
|
||||
}
|
28
src/Commands/Submodules.cs
Normal file
28
src/Commands/Submodules.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 获取子模块列表
|
||||
/// </summary>
|
||||
public class Submodules : Command {
|
||||
private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$");
|
||||
private List<string> modules = new List<string>();
|
||||
|
||||
public Submodules(string repo) {
|
||||
Cwd = repo;
|
||||
Args = "submodule status";
|
||||
}
|
||||
|
||||
public List<string> Result() {
|
||||
Exec();
|
||||
return modules;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
modules.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
42
src/Commands/Tag.cs
Normal file
42
src/Commands/Tag.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
|
||||
/// <summary>
|
||||
/// 标签相关指令
|
||||
/// </summary>
|
||||
public class Tag : Command {
|
||||
|
||||
public Tag(string repo) {
|
||||
Cwd = repo;
|
||||
}
|
||||
|
||||
public bool Add(string name, string basedOn, string message) {
|
||||
Args = $"tag -a {name} {basedOn} ";
|
||||
|
||||
if (!string.IsNullOrEmpty(message)) {
|
||||
string tmp = Path.GetTempFileName();
|
||||
File.WriteAllText(tmp, message);
|
||||
Args += $"-F \"{tmp}\"";
|
||||
} else {
|
||||
Args += $"-m {name}";
|
||||
}
|
||||
|
||||
return Exec();
|
||||
}
|
||||
|
||||
public bool Delete(string name, bool push) {
|
||||
Args = $"tag --delete {name}";
|
||||
if (!Exec()) return false;
|
||||
|
||||
if (push) {
|
||||
var remotes = new Remotes(Cwd).Result();
|
||||
foreach (var r in remotes) {
|
||||
new Push(Cwd, r.Name, name, true).Exec();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
45
src/Commands/Tags.cs
Normal file
45
src/Commands/Tags.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Commands {
|
||||
/// <summary>
|
||||
/// 解析所有的Tags
|
||||
/// </summary>
|
||||
public class Tags : Command {
|
||||
public static readonly string CMD = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
|
||||
public static readonly Regex REG_FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)");
|
||||
|
||||
private List<Models.Tag> loaded = new List<Models.Tag>();
|
||||
|
||||
public Tags(string path) {
|
||||
Cwd = path;
|
||||
Args = CMD;
|
||||
}
|
||||
|
||||
public List<Models.Tag> Result() {
|
||||
Exec();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public override void OnReadline(string line) {
|
||||
var match = REG_FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var name = match.Groups[1].Value;
|
||||
var commit = match.Groups[2].Value;
|
||||
var dereference = match.Groups[3].Value;
|
||||
|
||||
if (string.IsNullOrEmpty(dereference)) {
|
||||
loaded.Add(new Models.Tag() {
|
||||
Name = name,
|
||||
SHA = commit,
|
||||
});
|
||||
} else {
|
||||
loaded.Add(new Models.Tag() {
|
||||
Name = name,
|
||||
SHA = dereference,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Same as BoolToVisibilityConverter.
|
||||
/// </summary>
|
||||
public class BoolToCollapsed : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.Convert
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.ConvertBack
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Convert file status to brush
|
||||
/// </summary>
|
||||
public class FileStatusToColor : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Is only test local changes.
|
||||
/// </summary>
|
||||
public bool OnlyWorkTree { get; set; } = false;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var change = value as Git.Change;
|
||||
if (change == null) return Brushes.Transparent;
|
||||
|
||||
var status = Git.Change.Status.None;
|
||||
if (OnlyWorkTree) {
|
||||
if (change.IsConflit) return Brushes.Yellow;
|
||||
status = change.WorkTree;
|
||||
} else {
|
||||
status = change.Index;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Git.Change.Status.Modified: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90);
|
||||
case Git.Change.Status.Added: return new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90);
|
||||
case Git.Change.Status.Deleted: return new LinearGradientBrush(Colors.Tomato, Color.FromRgb(252, 165, 150), 90);
|
||||
case Git.Change.Status.Renamed: return new LinearGradientBrush(Colors.Orchid, Color.FromRgb(248, 161, 245), 90);
|
||||
case Git.Change.Status.Copied: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90);
|
||||
case Git.Change.Status.Unmerged: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90);
|
||||
case Git.Change.Status.Untracked: return new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90);
|
||||
default: return Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Convert file status to icon.
|
||||
/// </summary>
|
||||
public class FileStatusToIcon : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Is only test local changes.
|
||||
/// </summary>
|
||||
public bool OnlyWorkTree { get; set; } = false;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var change = value as Git.Change;
|
||||
if (change == null) return "";
|
||||
|
||||
var status = Git.Change.Status.None;
|
||||
if (OnlyWorkTree) {
|
||||
if (change.IsConflit) return "X";
|
||||
status = change.WorkTree;
|
||||
} else {
|
||||
status = change.Index;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Git.Change.Status.Modified: return "M";
|
||||
case Git.Change.Status.Added: return "A";
|
||||
case Git.Change.Status.Deleted: return "D";
|
||||
case Git.Change.Status.Renamed: return "R";
|
||||
case Git.Change.Status.Copied: return "C";
|
||||
case Git.Change.Status.Unmerged: return "U";
|
||||
case Git.Change.Status.Untracked: return "?";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
public class FilesDisplayModeToIcon : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var mode = (Preference.FilesDisplayMode)value;
|
||||
switch (mode) {
|
||||
case Preference.FilesDisplayMode.Grid:
|
||||
return App.Current.FindResource("Icon.Grid") as Geometry;
|
||||
case Preference.FilesDisplayMode.List:
|
||||
return App.Current.FindResource("Icon.List") as Geometry;
|
||||
default:
|
||||
return App.Current.FindResource("Icon.Tree") as Geometry;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
public class FilesDisplayModeToList : IValueConverter {
|
||||
|
||||
public bool TreatGridAsList { get; set; } = true;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var mode = (Preference.FilesDisplayMode)value;
|
||||
if (mode == Preference.FilesDisplayMode.Tree) return Visibility.Collapsed;
|
||||
if (mode == Preference.FilesDisplayMode.List) return Visibility.Visible;
|
||||
if (TreatGridAsList) return Visibility.Visible;
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class FilesDisplayModeToGrid : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return (Preference.FilesDisplayMode)value == Preference.FilesDisplayMode.Grid ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class FilesDisplayModeToTree : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return (Preference.FilesDisplayMode)value == Preference.FilesDisplayMode.Tree ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Convert indent(horizontal offset) to Margin property
|
||||
/// </summary>
|
||||
public class IndentToMargin : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.Convert
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return new Thickness((double)value, 0, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.ConvertBack
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return ((Thickness)value).Left;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Integer to color.
|
||||
/// </summary>
|
||||
public class IntToRepoColor : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// All supported colors.
|
||||
/// </summary>
|
||||
public static Brush[] Colors = new Brush[] {
|
||||
Brushes.Transparent,
|
||||
Brushes.White,
|
||||
Brushes.Red,
|
||||
Brushes.Orange,
|
||||
Brushes.Yellow,
|
||||
Brushes.ForestGreen,
|
||||
Brushes.Purple,
|
||||
Brushes.DeepSkyBlue,
|
||||
Brushes.Magenta,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.Convert
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return Colors[((int)value) % Colors.Length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.ConvertBack
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return ((Thickness)value).Left;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Inverse BoolToCollapsed.
|
||||
/// </summary>
|
||||
public class InverseBoolToCollapsed : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.Convert
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return (bool)value ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.ConvertBack
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
public class PathToFileName : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return Path.GetFileName(value as string);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class PathToFolderName : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
return Path.GetDirectoryName(value as string);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Converters {
|
||||
|
||||
/// <summary>
|
||||
/// Convert depth of a TreeViewItem to Margin property.
|
||||
/// </summary>
|
||||
public class TreeViewItemDepthToMargin : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Indent length
|
||||
/// </summary>
|
||||
public double Indent { get; set; } = 19;
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConverter.Convert
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
TreeViewItem item = value as TreeViewItem;
|
||||
if (item == null) return new Thickness(0);
|
||||
|
||||
TreeViewItem iterator = GetParent(item);
|
||||
int depth = 0;
|
||||
while (iterator != null) {
|
||||
depth++;
|
||||
iterator = GetParent(iterator);
|
||||
}
|
||||
|
||||
return new Thickness(Indent * depth, 0, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement IValueConvert.ConvertBack
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get parent item.
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
private TreeViewItem GetParent(TreeViewItem item) {
|
||||
var parent = VisualTreeHelper.GetParent(item);
|
||||
|
||||
while (parent != null && !(parent is TreeView) && !(parent is TreeViewItem)) {
|
||||
parent = VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
|
||||
return parent as TreeViewItem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Blame
|
||||
/// </summary>
|
||||
public class Blame {
|
||||
|
||||
/// <summary>
|
||||
/// Line content.
|
||||
/// </summary>
|
||||
public class Line {
|
||||
public string CommitSHA { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Time { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lines
|
||||
/// </summary>
|
||||
public List<Line> Lines { get; set; } = new List<Line>();
|
||||
|
||||
/// <summary>
|
||||
/// Is binary file?
|
||||
/// </summary>
|
||||
public bool IsBinary { get; set; } = false;
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Git branch
|
||||
/// </summary>
|
||||
public class Branch {
|
||||
private static readonly string PRETTY_FORMAT = @"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)";
|
||||
private static readonly Regex PARSE = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
|
||||
private static readonly Regex AHEAD = new Regex(@"ahead (\d+)");
|
||||
private static readonly Regex BEHIND = new Regex(@"behind (\d+)");
|
||||
|
||||
/// <summary>
|
||||
/// Branch type.
|
||||
/// </summary>
|
||||
public enum Type {
|
||||
Normal,
|
||||
Feature,
|
||||
Release,
|
||||
Hotfix,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Full name.
|
||||
/// </summary>
|
||||
public string FullName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Head ref
|
||||
/// </summary>
|
||||
public string Head { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Subject for head ref.
|
||||
/// </summary>
|
||||
public string HeadSubject { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Is local branch
|
||||
/// </summary>
|
||||
public bool IsLocal { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Branch type.
|
||||
/// </summary>
|
||||
public Type Kind { get; set; } = Type.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// Remote name. Only used for remote branch
|
||||
/// </summary>
|
||||
public string Remote { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Upstream. Only used for local branches.
|
||||
/// </summary>
|
||||
public string Upstream { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Track information for upstream. Only used for local branches.
|
||||
/// </summary>
|
||||
public string UpstreamTrack { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is current branch. Only used for local branches.
|
||||
/// </summary>
|
||||
public bool IsCurrent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this branch's HEAD same with upstream?
|
||||
/// </summary>
|
||||
public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
|
||||
|
||||
/// <summary>
|
||||
/// Enable filter in log histories.
|
||||
/// </summary>
|
||||
public bool IsFiltered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Load branches.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public static List<Branch> Load(Repository repo) {
|
||||
var localPrefix = "refs/heads/";
|
||||
var remotePrefix = "refs/remotes/";
|
||||
var branches = new List<Branch>();
|
||||
var remoteBranches = new List<string>();
|
||||
|
||||
repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => {
|
||||
var match = PARSE.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var branch = new Branch();
|
||||
var refname = match.Groups[1].Value;
|
||||
if (refname.EndsWith("/HEAD")) return;
|
||||
|
||||
if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) {
|
||||
branch.Name = refname.Substring(localPrefix.Length);
|
||||
branch.IsLocal = true;
|
||||
} else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) {
|
||||
var name = refname.Substring(remotePrefix.Length);
|
||||
if (name.Contains("/")) {
|
||||
branch.Remote = name.Substring(0, name.IndexOf('/'));
|
||||
} else {
|
||||
branch.Remote = name;
|
||||
}
|
||||
branch.Name = name;
|
||||
branch.IsLocal = false;
|
||||
remoteBranches.Add(refname);
|
||||
}
|
||||
|
||||
branch.FullName = refname;
|
||||
branch.Head = match.Groups[2].Value;
|
||||
branch.IsCurrent = match.Groups[3].Value == "*";
|
||||
branch.Upstream = match.Groups[4].Value;
|
||||
branch.UpstreamTrack = ParseTrack(match.Groups[5].Value);
|
||||
branch.HeadSubject = match.Groups[6].Value;
|
||||
|
||||
branches.Add(branch);
|
||||
});
|
||||
|
||||
// Fixed deleted remote branch
|
||||
foreach (var b in branches) {
|
||||
if (!string.IsNullOrEmpty(b.Upstream) && !remoteBranches.Contains(b.Upstream)) {
|
||||
b.Upstream = null;
|
||||
}
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new branch.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="startPoint"></param>
|
||||
public static void Create(Repository repo, string name, string startPoint) {
|
||||
var errs = repo.RunCommand($"branch {name} {startPoint}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename branch
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
public void Rename(Repository repo, string name) {
|
||||
var errs = repo.RunCommand($"branch -M {Name} {name}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change upstream
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="upstream"></param>
|
||||
public void SetUpstream(Repository repo, string upstream) {
|
||||
var errs = repo.RunCommand($"branch {Name} -u {upstream}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
|
||||
repo.Branches(true);
|
||||
repo.OnBranchChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete branch.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public void Delete(Repository repo) {
|
||||
string errs = null;
|
||||
|
||||
if (!IsLocal) {
|
||||
errs = repo.RunCommand($"-c credential.helper=manager push {Remote} --delete {Name.Substring(Name.IndexOf('/')+1)}", null);
|
||||
} else {
|
||||
errs = repo.RunCommand($"branch -D {Name}", null);
|
||||
}
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
private static string ParseTrack(string data) {
|
||||
if (string.IsNullOrEmpty(data)) return "";
|
||||
|
||||
string track = "";
|
||||
|
||||
var ahead = AHEAD.Match(data);
|
||||
if (ahead.Success) {
|
||||
track += ahead.Groups[1].Value + "↑ ";
|
||||
}
|
||||
|
||||
var behind = BEHIND.Match(data);
|
||||
if (behind.Success) {
|
||||
track += behind.Groups[1].Value + "↓";
|
||||
}
|
||||
|
||||
return track.Trim();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Changed file status.
|
||||
/// </summary>
|
||||
public class Change {
|
||||
private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
|
||||
|
||||
/// <summary>
|
||||
/// Status Code
|
||||
/// </summary>
|
||||
public enum Status {
|
||||
None,
|
||||
Modified,
|
||||
Added,
|
||||
Deleted,
|
||||
Renamed,
|
||||
Copied,
|
||||
Unmerged,
|
||||
Untracked,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index status
|
||||
/// </summary>
|
||||
public Status Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work tree status.
|
||||
/// </summary>
|
||||
public Status WorkTree { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current file path.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original file path before this revision.
|
||||
/// </summary>
|
||||
public string OriginalPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Staged(added) in index?
|
||||
/// </summary>
|
||||
public bool IsAddedToIndex {
|
||||
get {
|
||||
if (Index == Status.None || Index == Status.Untracked) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is conflict?
|
||||
/// </summary>
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse change for `--name-status` data.
|
||||
/// </summary>
|
||||
/// <param name="data">Raw data.</param>
|
||||
/// <param name="fromCommit">Read from commit?</param>
|
||||
/// <returns>Parsed change instance.</returns>
|
||||
public static Change Parse(string data, bool fromCommit = false) {
|
||||
var match = FORMAT.Match(data);
|
||||
if (!match.Success) return null;
|
||||
|
||||
var change = new Change() { Path = match.Groups[2].Value };
|
||||
var status = match.Groups[1].Value;
|
||||
|
||||
if (fromCommit) {
|
||||
switch (status[0]) {
|
||||
case 'M': change.Set(Status.Modified); break;
|
||||
case 'A': change.Set(Status.Added); break;
|
||||
case 'D': change.Set(Status.Deleted); break;
|
||||
case 'R': change.Set(Status.Renamed); break;
|
||||
case 'C': change.Set(Status.Copied); break;
|
||||
default: return null;
|
||||
}
|
||||
} else {
|
||||
switch (status) {
|
||||
case " M": change.Set(Status.None, Status.Modified); break;
|
||||
case " A": change.Set(Status.None, Status.Added); break;
|
||||
case " D": change.Set(Status.None, Status.Deleted); break;
|
||||
case " R": change.Set(Status.None, Status.Renamed); break;
|
||||
case " C": change.Set(Status.None, Status.Copied); break;
|
||||
case "M": change.Set(Status.Modified, Status.None); break;
|
||||
case "MM": change.Set(Status.Modified, Status.Modified); break;
|
||||
case "MD": change.Set(Status.Modified, Status.Deleted); break;
|
||||
case "A": change.Set(Status.Added, Status.None); break;
|
||||
case "AM": change.Set(Status.Added, Status.Modified); break;
|
||||
case "AD": change.Set(Status.Added, Status.Deleted); break;
|
||||
case "D": change.Set(Status.Deleted, Status.None); break;
|
||||
case "R": change.Set(Status.Renamed, Status.None); break;
|
||||
case "RM": change.Set(Status.Renamed, Status.Modified); break;
|
||||
case "RD": change.Set(Status.Renamed, Status.Deleted); break;
|
||||
case "C": change.Set(Status.Copied, Status.None); break;
|
||||
case "CM": change.Set(Status.Copied, Status.Modified); break;
|
||||
case "CD": change.Set(Status.Copied, Status.Deleted); break;
|
||||
case "DR": change.Set(Status.Deleted, Status.Renamed); break;
|
||||
case "DC": change.Set(Status.Deleted, Status.Copied); break;
|
||||
case "DD": change.Set(Status.Deleted, Status.Deleted); break;
|
||||
case "AU": change.Set(Status.Added, Status.Unmerged); break;
|
||||
case "UD": change.Set(Status.Unmerged, Status.Deleted); break;
|
||||
case "UA": change.Set(Status.Unmerged, Status.Added); break;
|
||||
case "DU": change.Set(Status.Deleted, Status.Unmerged); break;
|
||||
case "AA": change.Set(Status.Added, Status.Added); break;
|
||||
case "UU": change.Set(Status.Unmerged, Status.Unmerged); break;
|
||||
case "??": change.Set(Status.Untracked, Status.Untracked); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (change.Path[0] == '"') change.Path = change.Path.Substring(1, change.Path.Length - 2);
|
||||
if (!string.IsNullOrEmpty(change.OriginalPath) && change.OriginalPath[0] == '"') change.OriginalPath = change.OriginalPath.Substring(1, change.OriginalPath.Length - 2);
|
||||
return change;
|
||||
}
|
||||
|
||||
private void Set(Status index, Status workTree = Status.None) {
|
||||
Index = index;
|
||||
WorkTree = workTree;
|
||||
|
||||
if (index == Status.Renamed || workTree == Status.Renamed) {
|
||||
var idx = Path.IndexOf('\t');
|
||||
if (idx >= 0) {
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
Path = Path.Substring(idx + 1);
|
||||
} else {
|
||||
idx = Path.IndexOf(" -> ");
|
||||
if (idx > 0) {
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
Path = Path.Substring(idx + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,364 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Git commit information.
|
||||
/// </summary>
|
||||
public class Commit {
|
||||
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
|
||||
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
|
||||
private static readonly Regex REG_TESTBINARY = new Regex(@"^\-\s+\-\s+.*$");
|
||||
|
||||
/// <summary>
|
||||
/// Object in commit.
|
||||
/// </summary>
|
||||
public class Object {
|
||||
public enum Type {
|
||||
Tag,
|
||||
Blob,
|
||||
Tree,
|
||||
Commit,
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public Type Kind { get; set; }
|
||||
public string SHA { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Line of text in file.
|
||||
/// </summary>
|
||||
public class Line {
|
||||
public int No { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA
|
||||
/// </summary>
|
||||
public string SHA { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Short SHA.
|
||||
/// </summary>
|
||||
public string ShortSHA => SHA.Substring(0, 8);
|
||||
|
||||
/// <summary>
|
||||
/// Parent commit SHAs.
|
||||
/// </summary>
|
||||
public List<string> Parents { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Author
|
||||
/// </summary>
|
||||
public User Author { get; set; } = new User();
|
||||
|
||||
/// <summary>
|
||||
/// Committer.
|
||||
/// </summary>
|
||||
public User Committer { get; set; } = new User();
|
||||
|
||||
/// <summary>
|
||||
/// Subject
|
||||
/// </summary>
|
||||
public string Subject { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Extra message.
|
||||
/// </summary>
|
||||
public string Message { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// HEAD commit?
|
||||
/// </summary>
|
||||
public bool IsHEAD { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Merged in current branch?
|
||||
/// </summary>
|
||||
public bool IsMerged { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// X offset in graph
|
||||
/// </summary>
|
||||
public double GraphOffset { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Has decorators.
|
||||
/// </summary>
|
||||
public bool HasDecorators => Decorators.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Decorators.
|
||||
/// </summary>
|
||||
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
|
||||
|
||||
/// <summary>
|
||||
/// Read commits.
|
||||
/// </summary>
|
||||
/// <param name="repo">Repository</param>
|
||||
/// <param name="limit">Limitations</param>
|
||||
/// <returns>Parsed commits.</returns>
|
||||
public static List<Commit> Load(Repository repo, string limit) {
|
||||
List<Commit> commits = new List<Commit>();
|
||||
Commit current = null;
|
||||
bool bSkippingGpgsig = false;
|
||||
bool findHead = false;
|
||||
|
||||
repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => {
|
||||
if (bSkippingGpgsig) {
|
||||
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false;
|
||||
return;
|
||||
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) {
|
||||
bSkippingGpgsig = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
|
||||
if (current != null) {
|
||||
current.Message = current.Message.TrimEnd();
|
||||
commits.Add(current);
|
||||
}
|
||||
|
||||
current = new Commit();
|
||||
ParseSHA(current, line.Substring("commit ".Length));
|
||||
if (!findHead) findHead = current.IsHEAD;
|
||||
return;
|
||||
}
|
||||
|
||||
if (current == null) return;
|
||||
|
||||
if (line.StartsWith("tree ", StringComparison.Ordinal)) {
|
||||
return;
|
||||
} else if (line.StartsWith("parent ", StringComparison.Ordinal)) {
|
||||
current.Parents.Add(line.Substring("parent ".Length));
|
||||
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
|
||||
current.Author.Parse(line);
|
||||
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) {
|
||||
current.Committer.Parse(line);
|
||||
} else if (string.IsNullOrEmpty(current.Subject)) {
|
||||
current.Subject = line.Trim();
|
||||
} else {
|
||||
current.Message += (line.Trim() + "\n");
|
||||
}
|
||||
});
|
||||
|
||||
if (current != null) {
|
||||
current.Message = current.Message.TrimEnd();
|
||||
commits.Add(current);
|
||||
}
|
||||
|
||||
if (!findHead && commits.Count > 0) {
|
||||
if (commits[commits.Count - 1].IsAncestorOfHead(repo)) {
|
||||
if (commits.Count == 1) {
|
||||
commits[0].IsMerged = true;
|
||||
} else {
|
||||
var head = FindFirstMerged(repo, commits, 0, commits.Count - 1);
|
||||
if (head != null) head.IsMerged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get changed file list.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <returns></returns>
|
||||
public List<Change> GetChanges(Repository repo) {
|
||||
var changes = new List<Change>();
|
||||
var regex = new Regex(@"^[MADRC]\d*\s*.*$");
|
||||
|
||||
var errs = repo.RunCommand($"show --name-status {SHA}", line => {
|
||||
if (!regex.IsMatch(line)) return;
|
||||
|
||||
var change = Change.Parse(line, true);
|
||||
if (change != null) changes.Add(change);
|
||||
});
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get revision files.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <returns></returns>
|
||||
public List<Object> GetFiles(Repository repo) {
|
||||
var files = new List<Object>();
|
||||
var test = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
|
||||
|
||||
var errs = repo.RunCommand($"ls-tree -r {SHA}", line => {
|
||||
var match = test.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var obj = new Object();
|
||||
obj.Path = match.Groups[3].Value;
|
||||
obj.Kind = Object.Type.Blob;
|
||||
obj.SHA = match.Groups[2].Value;
|
||||
|
||||
switch (match.Groups[1].Value) {
|
||||
case "tag": obj.Kind = Object.Type.Tag; break;
|
||||
case "blob": obj.Kind = Object.Type.Blob; break;
|
||||
case "tree": obj.Kind = Object.Type.Tree; break;
|
||||
case "commit": obj.Kind = Object.Type.Commit; break;
|
||||
}
|
||||
|
||||
files.Add(obj);
|
||||
});
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
return files;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get file content.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="line"></param>
|
||||
/// <returns></returns>
|
||||
public bool GetTextFileContent(Repository repo, string file, List<Line> lines) {
|
||||
var binary = false;
|
||||
var count = 0;
|
||||
|
||||
repo.RunCommand($"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {SHA} --numstat -- \"{file}\"", line => {
|
||||
if (REG_TESTBINARY.IsMatch(line)) binary = true;
|
||||
});
|
||||
|
||||
if (!binary) {
|
||||
var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => {
|
||||
if (binary) return;
|
||||
|
||||
if (line.IndexOf('\0') >= 0) {
|
||||
binary = true;
|
||||
lines.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
count++;
|
||||
lines.Add(new Line() { No = count, Content = line });
|
||||
});
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
return binary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save file to.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="saveTo"></param>
|
||||
public void SaveFileTo(Repository repo, string file, string saveTo) {
|
||||
var tmp = Path.GetTempFileName();
|
||||
var bat = tmp + ".bat";
|
||||
var cmd = "";
|
||||
|
||||
if (repo.IsLFSFiltered(file)) {
|
||||
cmd += $"git --no-pager show {SHA}:\"{file}\" > {tmp}.lfs\n";
|
||||
cmd += $"git --no-pager lfs smudge < {tmp}.lfs > {saveTo}\n";
|
||||
} else {
|
||||
cmd = $"git --no-pager show {SHA}:\"{file}\" > {saveTo}\n";
|
||||
}
|
||||
|
||||
File.WriteAllText(bat, cmd);
|
||||
|
||||
var starter = new ProcessStartInfo();
|
||||
starter.FileName = bat;
|
||||
starter.WorkingDirectory = repo.Path;
|
||||
starter.CreateNoWindow = true;
|
||||
starter.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
|
||||
var proc = Process.Start(starter);
|
||||
proc.WaitForExit();
|
||||
proc.Close();
|
||||
|
||||
File.Delete(bat);
|
||||
}
|
||||
|
||||
private bool IsAncestorOfHead(Repository repo) {
|
||||
var startInfo = new ProcessStartInfo();
|
||||
startInfo.FileName = App.Setting.Tools.GitExecutable;
|
||||
startInfo.Arguments = $"merge-base --is-ancestor {SHA} HEAD";
|
||||
startInfo.WorkingDirectory = repo.Path;
|
||||
startInfo.UseShellExecute = false;
|
||||
startInfo.CreateNoWindow = true;
|
||||
startInfo.RedirectStandardOutput = false;
|
||||
startInfo.RedirectStandardError = false;
|
||||
|
||||
var proc = new Process() { StartInfo = startInfo };
|
||||
proc.Start();
|
||||
proc.WaitForExit();
|
||||
|
||||
var ret = proc.ExitCode;
|
||||
proc.Close();
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
private static Commit FindFirstMerged(Repository repo, List<Commit> commits, int start, int end) {
|
||||
var isStartAncestor = commits[start].IsAncestorOfHead(repo);
|
||||
if (isStartAncestor) return commits[start];
|
||||
|
||||
if (end - start <= 1) {
|
||||
return commits[end];
|
||||
} else {
|
||||
var mid = (int)Math.Floor((end + start) * 0.5f);
|
||||
if (commits[mid].IsAncestorOfHead(repo)) {
|
||||
return FindFirstMerged(repo, commits, start + 1, mid);
|
||||
} else {
|
||||
return FindFirstMerged(repo, commits, mid + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseSHA(Commit commit, string data) {
|
||||
var decoratorStart = data.IndexOf('(');
|
||||
if (decoratorStart < 0) {
|
||||
commit.SHA = data.Trim();
|
||||
return;
|
||||
}
|
||||
|
||||
commit.SHA = data.Substring(0, decoratorStart).Trim();
|
||||
|
||||
var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var sub in subs) {
|
||||
var d = sub.Trim();
|
||||
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) {
|
||||
commit.Decorators.Add(new Decorator() {
|
||||
Type = DecoratorType.Tag,
|
||||
Name = d.Substring(15).Trim()
|
||||
});
|
||||
} else if (d.EndsWith("/HEAD")) {
|
||||
continue;
|
||||
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) {
|
||||
commit.IsHEAD = true;
|
||||
commit.Decorators.Add(new Decorator() {
|
||||
Type = DecoratorType.CurrentBranchHead,
|
||||
Name = d.Substring(19).Trim()
|
||||
});
|
||||
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) {
|
||||
commit.Decorators.Add(new Decorator() {
|
||||
Type = DecoratorType.LocalBranchHead,
|
||||
Name = d.Substring(11).Trim()
|
||||
});
|
||||
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) {
|
||||
commit.Decorators.Add(new Decorator() {
|
||||
Type = DecoratorType.RemoteBranchHead,
|
||||
Name = d.Substring(13).Trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
188
src/Git/Diff.cs
188
src/Git/Diff.cs
|
@ -1,188 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Diff helper.
|
||||
/// </summary>
|
||||
public class Diff {
|
||||
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
|
||||
|
||||
/// <summary>
|
||||
/// Line mode.
|
||||
/// </summary>
|
||||
public enum LineMode {
|
||||
None,
|
||||
Normal,
|
||||
Indicator,
|
||||
Added,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Line change.
|
||||
/// </summary>
|
||||
public class LineChange {
|
||||
public LineMode Mode = LineMode.Normal;
|
||||
public string Content = "";
|
||||
public string OldLine = "";
|
||||
public string NewLine = "";
|
||||
|
||||
public LineChange(LineMode mode, string content, string oldLine = "", string newLine = "") {
|
||||
Mode = mode;
|
||||
Content = content;
|
||||
OldLine = oldLine;
|
||||
NewLine = newLine;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text change.
|
||||
/// </summary>
|
||||
public class TextChange {
|
||||
public List<LineChange> Lines = new List<LineChange>();
|
||||
public bool IsBinary = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary change.
|
||||
/// </summary>
|
||||
public class BinaryChange {
|
||||
public long Size = 0;
|
||||
public long PreSize = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change for LFS object information.
|
||||
/// </summary>
|
||||
public class LFSChange {
|
||||
public LFSObject Old;
|
||||
public LFSObject New;
|
||||
public bool IsValid => Old != null || New != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run diff process.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static TextChange GetTextChange(Repository repo, string args) {
|
||||
var rs = new TextChange();
|
||||
var started = false;
|
||||
var oldLine = 0;
|
||||
var newLine = 0;
|
||||
|
||||
repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => {
|
||||
if (rs.IsBinary) return;
|
||||
|
||||
if (!started) {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (!match.Success) {
|
||||
if (line.StartsWith("Binary ")) rs.IsBinary = true;
|
||||
return;
|
||||
}
|
||||
|
||||
started = true;
|
||||
oldLine = int.Parse(match.Groups[1].Value);
|
||||
newLine = int.Parse(match.Groups[2].Value);
|
||||
rs.Lines.Add(new LineChange(LineMode.Indicator, line));
|
||||
} else {
|
||||
if (line[0] == '-') {
|
||||
rs.Lines.Add(new LineChange(LineMode.Deleted, line.Substring(1), $"{oldLine}", ""));
|
||||
oldLine++;
|
||||
} else if (line[0] == '+') {
|
||||
rs.Lines.Add(new LineChange(LineMode.Added, line.Substring(1), "", $"{newLine}"));
|
||||
newLine++;
|
||||
} else if (line[0] == '\\') {
|
||||
// IGNORE \ No new line end of file.
|
||||
} else {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (match.Success) {
|
||||
oldLine = int.Parse(match.Groups[1].Value);
|
||||
newLine = int.Parse(match.Groups[2].Value);
|
||||
rs.Lines.Add(new LineChange(LineMode.Indicator, line));
|
||||
} else {
|
||||
rs.Lines.Add(new LineChange(LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}"));
|
||||
oldLine++;
|
||||
newLine++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rs.IsBinary) rs.Lines.Clear();
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get file size changes for binary file.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="revisions"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="orgPath"></param>
|
||||
/// <returns></returns>
|
||||
public static BinaryChange GetSizeChange(Repository repo, string[] revisions, string path, string orgPath = null) {
|
||||
var change = new BinaryChange();
|
||||
|
||||
if (revisions.Length == 0) { // Compare working copy with HEAD
|
||||
change.Size = new FileInfo(Path.Combine(repo.Path, path)).Length;
|
||||
change.PreSize = repo.GetFileSize("HEAD", path);
|
||||
} else if (revisions.Length == 1) { // Compare HEAD with given revision.
|
||||
change.Size = repo.GetFileSize("HEAD", path);
|
||||
if (!string.IsNullOrEmpty(orgPath)) {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], orgPath);
|
||||
} else {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], path);
|
||||
}
|
||||
} else {
|
||||
change.Size = repo.GetFileSize(revisions[1], path);
|
||||
if (!string.IsNullOrEmpty(orgPath)) {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], orgPath);
|
||||
} else {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], path);
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get LFS object changes.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static LFSChange GetLFSChange(Repository repo, string args) {
|
||||
var rc = new LFSChange();
|
||||
|
||||
repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => {
|
||||
if (line[0] == '-') {
|
||||
if (rc.Old == null) rc.Old = new LFSObject();
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
rc.Old.OID = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
rc.Old.Size = int.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (line[0] == '+') {
|
||||
if (rc.New == null) rc.New = new LFSObject();
|
||||
line = line.Substring(1);
|
||||
if (line.StartsWith("oid sha256:")) {
|
||||
rc.New.OID = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
rc.New.Size = int.Parse(line.Substring(5));
|
||||
}
|
||||
} else if (line.StartsWith(" size ")) {
|
||||
rc.New.Size = rc.Old.Size = int.Parse(line.Substring(6));
|
||||
}
|
||||
});
|
||||
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Object filtered by LFS
|
||||
/// </summary>
|
||||
public class LFSObject {
|
||||
|
||||
/// <summary>
|
||||
/// Object id
|
||||
/// </summary>
|
||||
public string OID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Object size.
|
||||
/// </summary>
|
||||
public long Size { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
using Microsoft.Win32;
|
||||
using SourceGit.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// External merge tool
|
||||
/// </summary>
|
||||
public class MergeTool {
|
||||
|
||||
/// <summary>
|
||||
/// Display name
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Executable file name.
|
||||
/// </summary>
|
||||
public string ExecutableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Command line parameter.
|
||||
/// </summary>
|
||||
public string Parameter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Auto finder.
|
||||
/// </summary>
|
||||
public Func<string> Finder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this merge tool configured.
|
||||
/// </summary>
|
||||
public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName);
|
||||
|
||||
/// <summary>
|
||||
/// Supported merge tools.
|
||||
/// </summary>
|
||||
public static List<MergeTool> Supported = new List<MergeTool>() {
|
||||
new MergeTool("--", "", "", FindInvalid),
|
||||
new MergeTool("Araxis Merge", "Compare.exe", "/wait /merge /3 /a1 \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindAraxisMerge),
|
||||
new MergeTool("Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare),
|
||||
new MergeTool("KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3),
|
||||
new MergeTool("P4Merge", "p4merge.exe", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindP4Merge),
|
||||
new MergeTool("Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge),
|
||||
new MergeTool("Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge),
|
||||
new MergeTool("Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Finder for invalid merge tool.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string FindInvalid() {
|
||||
return "--";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find araxis merge tool install path.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string FindAraxisMerge() {
|
||||
var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe";
|
||||
if (File.Exists(path)) return path;
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find kdiff3.exe by registry.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finder for p4merge
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string FindP4Merge() {
|
||||
var path = @"C:\Program Files\Perforce\p4merge.exe";
|
||||
if (File.Exists(path)) return path;
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find BComp.exe by registry.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public 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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find TortoiseMerge.exe by registry.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string FindTortoiseMerge() {
|
||||
var root = RegistryKey.OpenBaseKey(
|
||||
RegistryHive.LocalMachine,
|
||||
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
|
||||
|
||||
var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN");
|
||||
if (tortoiseSVN == null) return "";
|
||||
return tortoiseSVN.GetValue("TMergePath") as string;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find vsDiffMerge.exe.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public 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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find VSCode executable file path.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public 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 "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="exe"></param>
|
||||
/// <param name="param"></param>
|
||||
/// <param name="finder"></param>
|
||||
public MergeTool(string name, string exe, string param, Func<string> finder) {
|
||||
Name = name;
|
||||
ExecutableName = exe;
|
||||
Parameter = param;
|
||||
Finder = finder;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Git remote
|
||||
/// </summary>
|
||||
public class Remote {
|
||||
private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
|
||||
|
||||
/// <summary>
|
||||
/// Name of this remote
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL
|
||||
/// </summary>
|
||||
public string URL { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parsing remote
|
||||
/// </summary>
|
||||
/// <param name="repo">Repository</param>
|
||||
/// <returns></returns>
|
||||
public static List<Remote> Load(Repository repo) {
|
||||
var remotes = new List<Remote>();
|
||||
var added = new List<string>();
|
||||
|
||||
repo.RunCommand("remote -v", data => {
|
||||
var match = FORMAT.Match(data);
|
||||
if (!match.Success) return;
|
||||
|
||||
var remote = new Remote() {
|
||||
Name = match.Groups[1].Value,
|
||||
URL = match.Groups[2].Value,
|
||||
};
|
||||
|
||||
if (added.Contains(remote.Name)) return;
|
||||
|
||||
added.Add(remote.Name);
|
||||
remotes.Add(remote);
|
||||
});
|
||||
|
||||
return remotes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add new remote
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="url"></param>
|
||||
public static void Add(Repository repo, string name, string url) {
|
||||
var errs = repo.RunCommand($"remote add {name} {url}", null);
|
||||
if (errs != null) {
|
||||
App.RaiseError(errs);
|
||||
} else {
|
||||
repo.Fetch(new Remote() { Name = name }, true, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete remote.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="remote"></param>
|
||||
public static void Delete(Repository repo, string remote) {
|
||||
var errs = repo.RunCommand($"remote remove {remote}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edit remote.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="url"></param>
|
||||
public void Edit(Repository repo, string name, string url) {
|
||||
string errs = null;
|
||||
|
||||
if (name != Name) {
|
||||
errs = repo.RunCommand($"remote rename {Name} {name}", null);
|
||||
if (errs != null) {
|
||||
App.RaiseError(errs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (url != URL) {
|
||||
errs = repo.RunCommand($"remote set-url {name} {url}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
101
src/Git/Stash.cs
101
src/Git/Stash.cs
|
@ -1,101 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Git stash
|
||||
/// </summary>
|
||||
public class Stash {
|
||||
|
||||
/// <summary>
|
||||
/// SHA for this stash
|
||||
/// </summary>
|
||||
public string SHA { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Author
|
||||
/// </summary>
|
||||
public User Author { get; set; } = new User();
|
||||
|
||||
/// <summary>
|
||||
/// Message
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stash push.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="includeUntracked"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="files"></param>
|
||||
public static void Push(Repository repo, bool includeUntracked, string message, List<string> files) {
|
||||
string specialFiles = "";
|
||||
|
||||
if (files.Count > 0) {
|
||||
specialFiles = " --";
|
||||
foreach (var f in files) specialFiles += $" \"{f}\"";
|
||||
}
|
||||
|
||||
string args = "stash push ";
|
||||
if (includeUntracked) args += "-u ";
|
||||
if (!string.IsNullOrEmpty(message)) args += $"-m \"{message}\" ";
|
||||
|
||||
var errs = repo.RunCommand(args + specialFiles, null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get changed file list in this stash.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <returns></returns>
|
||||
public List<Change> GetChanges(Repository repo) {
|
||||
List<Change> changes = new List<Change>();
|
||||
|
||||
var errs = repo.RunCommand($"diff --name-status --pretty=format: {SHA}^ {SHA}", line => {
|
||||
var change = Change.Parse(line);
|
||||
if (change != null) changes.Add(change);
|
||||
});
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply stash.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public void Apply(Repository repo) {
|
||||
var errs = repo.RunCommand($"stash apply -q {Name}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pop stash
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public void Pop(Repository repo) {
|
||||
var errs = repo.RunCommand($"stash pop -q {Name}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop stash
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public void Drop(Repository repo) {
|
||||
var errs = repo.RunCommand($"stash drop -q {Name}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
}
|
||||
}
|
119
src/Git/Tag.cs
119
src/Git/Tag.cs
|
@ -1,119 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Git tag.
|
||||
/// </summary>
|
||||
public class Tag {
|
||||
private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)");
|
||||
|
||||
/// <summary>
|
||||
/// SHA
|
||||
/// </summary>
|
||||
public string SHA { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable filter in log histories.
|
||||
/// </summary>
|
||||
public bool IsFiltered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Load all tags
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <returns></returns>
|
||||
public static List<Tag> Load(Repository repo) {
|
||||
var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
|
||||
var tags = new List<Tag>();
|
||||
|
||||
repo.RunCommand(args, line => {
|
||||
var match = FORMAT.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var name = match.Groups[1].Value;
|
||||
var commit = match.Groups[2].Value;
|
||||
var dereference = match.Groups[3].Value;
|
||||
|
||||
if (string.IsNullOrEmpty(dereference)) {
|
||||
tags.Add(new Tag() {
|
||||
Name = name,
|
||||
SHA = commit,
|
||||
});
|
||||
} else {
|
||||
tags.Add(new Tag() {
|
||||
Name = name,
|
||||
SHA = dereference,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add new tag.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="startPoint"></param>
|
||||
/// <param name="message"></param>
|
||||
public static void Add(Repository repo, string name, string startPoint, string message) {
|
||||
var args = $"tag -a {name} {startPoint} ";
|
||||
|
||||
if (!string.IsNullOrEmpty(message)) {
|
||||
string temp = Path.GetTempFileName();
|
||||
File.WriteAllText(temp, message);
|
||||
args += $"-F \"{temp}\"";
|
||||
} else {
|
||||
args += $"-m {name}";
|
||||
}
|
||||
|
||||
var errs = repo.RunCommand(args, null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
else repo.OnCommitsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete tag.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="push"></param>
|
||||
public static void Delete(Repository repo, string name, bool push) {
|
||||
var errs = repo.RunCommand($"tag --delete {name}", null);
|
||||
if (errs != null) {
|
||||
App.RaiseError(errs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (push) {
|
||||
var remotes = repo.Remotes();
|
||||
foreach (var r in remotes) {
|
||||
repo.RunCommand($"-c credential.helper=manager push --delete {r.Name} refs/tags/{name}", null);
|
||||
}
|
||||
}
|
||||
|
||||
repo.LogFilters.Remove(name);
|
||||
repo.OnCommitsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push tag to remote.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="remote"></param>
|
||||
public static void Push(Repository repo, string name, string remote) {
|
||||
var errs = repo.RunCommand($"-c credential.helper=manager push {remote} refs/tags/{name}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Version information.
|
||||
/// </summary>
|
||||
public class Version {
|
||||
[JsonPropertyName("id")]
|
||||
public ulong Id { get; set; }
|
||||
[JsonPropertyName("tag_name")]
|
||||
public string TagName { get; set; }
|
||||
[JsonPropertyName("target_commitish")]
|
||||
public string CommitSHA { get; set; }
|
||||
[JsonPropertyName("prerelease")]
|
||||
public bool PreRelease { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,378 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Helpers {
|
||||
|
||||
/// <summary>
|
||||
/// Tools to parse commit graph.
|
||||
/// </summary>
|
||||
public class CommitGraphData {
|
||||
/// <summary>
|
||||
/// Unit lengths for commit graph
|
||||
/// </summary>
|
||||
public static readonly double UNIT_WIDTH = 12;
|
||||
public static readonly double HALF_WIDTH = 6;
|
||||
public static readonly double UNIT_HEIGHT = 24;
|
||||
public static readonly double HALF_HEIGHT = 12;
|
||||
|
||||
/// <summary>
|
||||
/// Colors
|
||||
/// </summary>
|
||||
public static Brush[] Colors = new Brush[] {
|
||||
Brushes.Orange,
|
||||
Brushes.ForestGreen,
|
||||
Brushes.Gold,
|
||||
Brushes.Magenta,
|
||||
Brushes.Red,
|
||||
Brushes.Gray,
|
||||
Brushes.Turquoise,
|
||||
Brushes.Olive,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Data to draw lines.
|
||||
/// </summary>
|
||||
public class Line {
|
||||
private double lastX = 0;
|
||||
private double lastY = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Parent commit id.
|
||||
/// </summary>
|
||||
public string Next { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is merged into this tree.
|
||||
/// </summary>
|
||||
public bool IsMerged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Points in line
|
||||
/// </summary>
|
||||
public List<Point> Points { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Brush to draw line
|
||||
/// </summary>
|
||||
public Brush Brush { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current horizontal offset.
|
||||
/// </summary>
|
||||
public double HorizontalOffset => lastX;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="nextCommitId">Parent commit id</param>
|
||||
/// <param name="isMerged">Is merged in tree</param>
|
||||
/// <param name="colorIdx">Color index</param>
|
||||
/// <param name="startPoint">Start point</param>
|
||||
public Line(string nextCommitId, bool isMerged, int colorIdx, Point startPoint) {
|
||||
Next = nextCommitId;
|
||||
IsMerged = isMerged;
|
||||
Points = new List<Point>() { startPoint };
|
||||
Brush = Colors[colorIdx % Colors.Length];
|
||||
|
||||
lastX = startPoint.X;
|
||||
lastY = startPoint.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Line to.
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <param name="isEnd"></param>
|
||||
public void AddPoint(double x, double y, bool isEnd = false) {
|
||||
if (x > lastX) {
|
||||
Points.Add(new Point(lastX, lastY));
|
||||
Points.Add(new Point(x, y - HALF_HEIGHT));
|
||||
} else if (x < lastX) {
|
||||
Points.Add(new Point(lastX, lastY + HALF_HEIGHT));
|
||||
Points.Add(new Point(x, y));
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
|
||||
if (isEnd) {
|
||||
var last = Points.Last();
|
||||
if (last.X != lastX || last.Y != lastY) Points.Add(new Point(lastX, lastY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Short link between two commits.
|
||||
/// </summary>
|
||||
public struct ShortLink {
|
||||
public Point Start;
|
||||
public Point Control;
|
||||
public Point End;
|
||||
public Brush Brush;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dot
|
||||
/// </summary>
|
||||
public struct Dot {
|
||||
public Point Center;
|
||||
public Brush Color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Independent lines in graph
|
||||
/// </summary>
|
||||
public List<Line> Lines { get; set; } = new List<Line>();
|
||||
|
||||
/// <summary>
|
||||
/// Short links.
|
||||
/// </summary>
|
||||
public List<ShortLink> Links { get; set; } = new List<ShortLink>();
|
||||
|
||||
/// <summary>
|
||||
/// All dots.
|
||||
/// </summary>
|
||||
public List<Dot> Dots { get; set; } = new List<Dot>();
|
||||
|
||||
/// <summary>
|
||||
/// Parse commits.
|
||||
/// </summary>
|
||||
/// <param name="commits"></param>
|
||||
/// <returns></returns>
|
||||
public static CommitGraphData Parse(List<Git.Commit> commits) {
|
||||
CommitGraphData data = new CommitGraphData();
|
||||
|
||||
List<Line> unsolved = new List<Line>();
|
||||
List<Line> ended = new List<Line>();
|
||||
Dictionary<string, Line> currentMap = new Dictionary<string, Line>();
|
||||
double offsetY = -HALF_HEIGHT;
|
||||
int colorIdx = 0;
|
||||
|
||||
for (int i = 0; i < commits.Count; i++) {
|
||||
Git.Commit commit = commits[i];
|
||||
Line major = null;
|
||||
bool isMerged = commit.IsHEAD || commit.IsMerged;
|
||||
int oldCount = unsolved.Count;
|
||||
|
||||
// 更新Y坐标
|
||||
offsetY += UNIT_HEIGHT;
|
||||
|
||||
// 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合)
|
||||
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 (!currentMap.ContainsKey(major.Next)) currentMap.Add(major.Next, major);
|
||||
} else {
|
||||
major.Next = "ENDED";
|
||||
ended.Add(l);
|
||||
}
|
||||
|
||||
major.AddPoint(offsetX, offsetY);
|
||||
} else {
|
||||
ended.Add(l);
|
||||
}
|
||||
|
||||
isMerged = isMerged || l.IsMerged;
|
||||
} else {
|
||||
if (!currentMap.ContainsKey(l.Next)) currentMap.Add(l.Next, l);
|
||||
offsetX += UNIT_WIDTH;
|
||||
l.AddPoint(offsetX, offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理本提交为非当前分支HEAD的情况(创建新依赖线路)
|
||||
if (major == null && commit.Parents.Count > 0) {
|
||||
offsetX += UNIT_WIDTH;
|
||||
major = new Line(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
|
||||
unsolved.Add(major);
|
||||
colorIdx++;
|
||||
}
|
||||
|
||||
// 确定本提交的点的位置
|
||||
Point position = new Point(offsetX, offsetY);
|
||||
if (major != null) {
|
||||
major.IsMerged = isMerged;
|
||||
position.X = major.HorizontalOffset;
|
||||
position.Y = offsetY;
|
||||
data.Dots.Add(new Dot() { Center = position, Color = major.Brush });
|
||||
} else {
|
||||
data.Dots.Add(new Dot() { Center = position, Color = Brushes.Orange });
|
||||
}
|
||||
|
||||
// 处理本提交的其他依赖
|
||||
for (int j = 1; j < commit.Parents.Count; j++) {
|
||||
var parent = commit.Parents[j];
|
||||
if (currentMap.ContainsKey(parent)) {
|
||||
var l = currentMap[parent];
|
||||
var link = new ShortLink();
|
||||
|
||||
link.Start = position;
|
||||
link.End = new Point(l.HorizontalOffset, offsetY + HALF_HEIGHT);
|
||||
link.Control = new Point(link.End.X, link.Start.Y);
|
||||
link.Brush = l.Brush;
|
||||
data.Links.Add(link);
|
||||
} else {
|
||||
offsetX += UNIT_WIDTH;
|
||||
unsolved.Add(new Line(commit.Parents[j], isMerged, colorIdx, position));
|
||||
colorIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理已终止的线
|
||||
foreach (var l in ended) {
|
||||
l.AddPoint(position.X, position.Y, true);
|
||||
data.Lines.Add(l);
|
||||
unsolved.Remove(l);
|
||||
}
|
||||
|
||||
// 加入本次提交
|
||||
commit.IsMerged = isMerged;
|
||||
commit.GraphOffset = Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH);
|
||||
|
||||
// 清理临时数据
|
||||
ended.Clear();
|
||||
currentMap.Clear();
|
||||
}
|
||||
|
||||
// 处理尚未终结的线
|
||||
for (int i = 0; i < unsolved.Count; i++) {
|
||||
var path = unsolved[i];
|
||||
var endY = (commits.Count - 0.5) * UNIT_HEIGHT;
|
||||
|
||||
if (path.Points.Count == 1 && path.Points[0].Y == endY) continue;
|
||||
|
||||
path.AddPoint((i + 0.5) * UNIT_WIDTH, endY, true);
|
||||
data.Lines.Add(path);
|
||||
}
|
||||
unsolved.Clear();
|
||||
|
||||
data.Lines.Sort((l, h) => l.Points[0].Y.CompareTo(h.Points[0].Y));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visual element to render commit graph
|
||||
/// </summary>
|
||||
public class CommitGraph : FrameworkElement {
|
||||
private double offsetY;
|
||||
private CommitGraphData data;
|
||||
|
||||
public CommitGraph() {
|
||||
Clear();
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
offsetY = 0;
|
||||
data = null;
|
||||
}
|
||||
|
||||
public void SetCommits(List<Git.Commit> commits) {
|
||||
data = CommitGraphData.Parse(commits);
|
||||
Dispatcher.Invoke(() => InvalidateVisual());
|
||||
}
|
||||
|
||||
public void SetOffset(double y) {
|
||||
offsetY = y * CommitGraphData.UNIT_HEIGHT;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext dc) {
|
||||
if (data == null) return;
|
||||
|
||||
var startY = offsetY;
|
||||
var endY = offsetY + ActualHeight;
|
||||
|
||||
dc.PushTransform(new TranslateTransform(0, -offsetY));
|
||||
|
||||
// Draw all visible lines.
|
||||
foreach (var path in data.Lines) {
|
||||
var last = path.Points[0];
|
||||
var size = path.Points.Count;
|
||||
|
||||
if (path.Points[size - 1].Y < startY) continue;
|
||||
if (last.Y > endY) break;
|
||||
|
||||
var geo = new StreamGeometry();
|
||||
var pen = new Pen(path.Brush, 2);
|
||||
|
||||
using (var geoCtx = geo.Open()) {
|
||||
geoCtx.BeginFigure(last, false, false);
|
||||
|
||||
var ended = false;
|
||||
for (int i = 1; i < size; i++) {
|
||||
var cur = path.Points[i];
|
||||
|
||||
// Fix line NOT shown in graph if cur.Y is too large than current.
|
||||
if (cur.Y > endY) {
|
||||
cur.Y = endY;
|
||||
ended = true;
|
||||
}
|
||||
|
||||
if (cur.X > last.X) {
|
||||
geoCtx.QuadraticBezierTo(new Point(cur.X, last.Y), cur, true, false);
|
||||
} else if (cur.X < last.X) {
|
||||
if (i < size - 1) {
|
||||
cur.Y += CommitGraphData.HALF_HEIGHT;
|
||||
|
||||
var midY = (last.Y + cur.Y) / 2;
|
||||
var midX = (last.X + cur.X) / 2;
|
||||
geoCtx.PolyQuadraticBezierTo(new Point[] {
|
||||
new Point(last.X, midY),
|
||||
new Point(midX, midY),
|
||||
new Point(cur.X, midY),
|
||||
cur}, true, false);
|
||||
} else {
|
||||
geoCtx.QuadraticBezierTo(new Point(last.X, cur.Y), cur, true, false);
|
||||
}
|
||||
} else {
|
||||
geoCtx.LineTo(cur, true, false);
|
||||
}
|
||||
|
||||
if (ended) break;
|
||||
last = cur;
|
||||
}
|
||||
}
|
||||
|
||||
geo.Freeze();
|
||||
dc.DrawGeometry(null, pen, geo);
|
||||
}
|
||||
|
||||
// Draw short links
|
||||
foreach (var link in data.Links) {
|
||||
if (link.End.Y < startY) continue;
|
||||
if (link.Start.Y > endY) break;
|
||||
|
||||
var geo = new StreamGeometry();
|
||||
var pen = new Pen(link.Brush, 2);
|
||||
|
||||
using (var geoCtx = geo.Open()) {
|
||||
geoCtx.BeginFigure(link.Start, false, false);
|
||||
geoCtx.QuadraticBezierTo(link.Control, link.End, true, false);
|
||||
}
|
||||
|
||||
geo.Freeze();
|
||||
dc.DrawGeometry(null, pen, geo);
|
||||
}
|
||||
|
||||
// Draw visible points
|
||||
foreach (var dot in data.Dots) {
|
||||
if (dot.Center.Y < startY) continue;
|
||||
if (dot.Center.Y > endY) break;
|
||||
|
||||
dc.DrawEllipse(dot.Color, null, dot.Center, 3, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Helpers {
|
||||
|
||||
/// <summary>
|
||||
/// Attached properties to TextBox.
|
||||
/// </summary>
|
||||
public static class TextBoxHelper {
|
||||
|
||||
/// <summary>
|
||||
/// Auto scroll on text changed or selection changed.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.RegisterAttached(
|
||||
"AutoScroll",
|
||||
typeof(bool),
|
||||
typeof(TextBoxHelper),
|
||||
new PropertyMetadata(false, OnAutoScrollChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder property
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
|
||||
"Placeholder",
|
||||
typeof(string),
|
||||
typeof(TextBoxHelper),
|
||||
new PropertyMetadata(string.Empty, OnPlaceholderChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Vertical alignment for placeholder.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.RegisterAttached(
|
||||
"PlaceholderBaseline",
|
||||
typeof(AlignmentY),
|
||||
typeof(TextBoxHelper),
|
||||
new PropertyMetadata(AlignmentY.Center));
|
||||
|
||||
/// <summary>
|
||||
/// Property to store generated placeholder brush.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty PlaceholderBrushProperty = DependencyProperty.RegisterAttached(
|
||||
"PlaceholderBrush",
|
||||
typeof(Brush),
|
||||
typeof(TextBoxHelper),
|
||||
new PropertyMetadata(Brushes.Transparent));
|
||||
|
||||
/// <summary>
|
||||
/// Setter for AutoScrollProperty
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="enabled"></param>
|
||||
public static void SetAutoScroll(UIElement element, bool enabled) {
|
||||
element.SetValue(AutoScrollProperty, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Getter for AutoScrollProperty
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
public static bool GetAutoScroll(UIElement element) {
|
||||
return (bool)element.GetValue(AutoScrollProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when AutoScroll property changed.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="e"></param>
|
||||
public static void OnAutoScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
var textBox = d as TextBox;
|
||||
if (textBox == null) return;
|
||||
|
||||
textBox.SelectionChanged -= UpdateScrollOnSelectionChanged;
|
||||
if ((bool)e.NewValue == true) {
|
||||
textBox.SelectionChanged += UpdateScrollOnSelectionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when placeholder changed.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
var textBox = d as TextBox;
|
||||
if (textBox != null) textBox.Loaded += OnTextLoaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter for Placeholder property
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void SetPlaceholder(UIElement element, string value) {
|
||||
element.SetValue(PlaceholderProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Getter for Placeholder property
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetPlaceholder(UIElement element) {
|
||||
return (string)element.GetValue(PlaceholderProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter for PlaceholderBaseline property
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="align"></param>
|
||||
public static void SetPlaceholderBaseline(UIElement element, AlignmentY align) {
|
||||
element.SetValue(PlaceholderBaselineProperty, align);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter for PlaceholderBaseline property.
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
public static AlignmentY GetPlaceholderBaseline(UIElement element) {
|
||||
return (AlignmentY)element.GetValue(PlaceholderBaselineProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter for PlaceholderBrush property.
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void SetPlaceholderBrush(UIElement element, Brush value) {
|
||||
element.SetValue(PlaceholderBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Getter for PlaceholderBrush property.
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
public static Brush GetPlaceholderBrush(UIElement element) {
|
||||
return (Brush)element.GetValue(PlaceholderBrushProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set placeholder as background when TextBox was loaded.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void OnTextLoaded(object sender, RoutedEventArgs e) {
|
||||
var textBox = sender as TextBox;
|
||||
if (textBox == null) return;
|
||||
|
||||
Label placeholder = new Label();
|
||||
placeholder.Content = textBox.GetValue(PlaceholderProperty);
|
||||
|
||||
VisualBrush brush = new VisualBrush();
|
||||
brush.AlignmentX = AlignmentX.Left;
|
||||
brush.AlignmentY = GetPlaceholderBaseline(textBox);
|
||||
brush.TileMode = TileMode.None;
|
||||
brush.Stretch = Stretch.None;
|
||||
brush.Opacity = 0.3;
|
||||
brush.Visual = placeholder;
|
||||
|
||||
textBox.SetValue(PlaceholderBrushProperty, brush);
|
||||
textBox.Background = brush;
|
||||
textBox.TextChanged += UpdatePlaceholder;
|
||||
UpdatePlaceholder(textBox, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically hide/show placeholder.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void UpdatePlaceholder(object sender, RoutedEventArgs e) {
|
||||
var textBox = sender as TextBox;
|
||||
if (string.IsNullOrEmpty(textBox.Text)) {
|
||||
textBox.Background = textBox.GetValue(PlaceholderBrushProperty) as Brush;
|
||||
} else {
|
||||
textBox.Background = Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void UpdateScrollOnSelectionChanged(object sender, RoutedEventArgs e) {
|
||||
var textBox = sender as TextBox;
|
||||
if (textBox != null && textBox.IsFocused) {
|
||||
if (Mouse.LeftButton == MouseButtonState.Pressed && textBox.SelectionLength > 0) {
|
||||
var p = Mouse.GetPosition(textBox);
|
||||
if (p.X <= 8) {
|
||||
textBox.LineLeft();
|
||||
} else if (p.X >= textBox.ActualWidth - 8) {
|
||||
textBox.LineRight();
|
||||
}
|
||||
|
||||
if (p.Y <= 8) {
|
||||
textBox.LineUp();
|
||||
} else if (p.Y >= textBox.ActualHeight - 8) {
|
||||
textBox.LineDown();
|
||||
}
|
||||
} else {
|
||||
var rect = textBox.GetRectFromCharacterIndex(textBox.CaretIndex);
|
||||
if (rect.Left <= 0) {
|
||||
textBox.ScrollToHorizontalOffset(textBox.HorizontalOffset + rect.Left);
|
||||
} else if (rect.Right >= textBox.ActualWidth) {
|
||||
textBox.ScrollToHorizontalOffset(textBox.HorizontalOffset + rect.Right);
|
||||
}
|
||||
|
||||
if (rect.Top <= 0) {
|
||||
textBox.ScrollToVerticalOffset(textBox.VerticalOffset + rect.Top);
|
||||
} else if (rect.Bottom >= textBox.ActualHeight) {
|
||||
textBox.ScrollToVerticalOffset(textBox.VerticalOffset + rect.Bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,350 +0,0 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Helpers {
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to enable multi-selection of TreeView
|
||||
/// </summary>
|
||||
public static class TreeViewHelper {
|
||||
|
||||
/// <summary>
|
||||
/// Definition of EnableMultiSelection property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty EnableMultiSelectionProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"EnableMultiSelection",
|
||||
typeof(bool),
|
||||
typeof(TreeViewHelper),
|
||||
new FrameworkPropertyMetadata(false, OnEnableMultiSelectionChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Getter of EnableMultiSelection
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public static bool GetEnableMultiSelection(DependencyObject obj) {
|
||||
return (bool)obj.GetValue(EnableMultiSelectionProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter of EnableMultiSelection
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void SetEnableMultiSelection(DependencyObject obj, bool value) {
|
||||
obj.SetValue(EnableMultiSelectionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Definition of SelectedItems
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty SelectedItemsProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"SelectedItems",
|
||||
typeof(ObservableCollection<TreeViewItem>),
|
||||
typeof(TreeViewHelper),
|
||||
new FrameworkPropertyMetadata(null));
|
||||
|
||||
/// <summary>
|
||||
/// Getter of SelectedItems
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public static ObservableCollection<TreeViewItem> GetSelectedItems(DependencyObject obj) {
|
||||
return (ObservableCollection<TreeViewItem>)obj.GetValue(SelectedItemsProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter of SelectedItems
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void SetSelectedItems(DependencyObject obj, ObservableCollection<TreeViewItem> value) {
|
||||
obj.SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Definition of IsChecked property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsCheckedProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsChecked",
|
||||
typeof(bool),
|
||||
typeof(TreeViewHelper),
|
||||
new FrameworkPropertyMetadata(false));
|
||||
|
||||
/// <summary>
|
||||
/// Getter of IsChecked Property.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public static bool GetIsChecked(DependencyObject obj) {
|
||||
return (bool)obj.GetValue(IsCheckedProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setter of IsChecked property
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void SetIsChecked(DependencyObject obj, bool value) {
|
||||
obj.SetValue(IsCheckedProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Definition of MultiSelectionChangedEvent
|
||||
/// </summary>
|
||||
public static readonly RoutedEvent MultiSelectionChangedEvent =
|
||||
EventManager.RegisterRoutedEvent("MultiSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeViewHelper));
|
||||
|
||||
/// <summary>
|
||||
/// Add handler for MultiSelectionChanged event.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="handler"></param>
|
||||
public static void AddMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
|
||||
var tree = d as TreeView;
|
||||
if (tree != null) tree.AddHandler(MultiSelectionChangedEvent, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove handler for MultiSelectionChanged event.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="handler"></param>
|
||||
public static void RemoveMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
|
||||
var tree = d as TreeView;
|
||||
if (tree != null) tree.RemoveHandler(MultiSelectionChangedEvent, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find ScrollViewer of a tree view
|
||||
/// </summary>
|
||||
/// <param name="owner"></param>
|
||||
/// <returns></returns>
|
||||
public static ScrollViewer GetScrollViewer(FrameworkElement owner) {
|
||||
if (owner == null) return null;
|
||||
if (owner is ScrollViewer) return owner as ScrollViewer;
|
||||
|
||||
int n = VisualTreeHelper.GetChildrenCount(owner);
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
|
||||
var deep = GetScrollViewer(child);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select all items in tree.
|
||||
/// </summary>
|
||||
/// <param name="tree"></param>
|
||||
public static void SelectWholeTree(TreeView tree) {
|
||||
var selected = GetSelectedItems(tree);
|
||||
selected.Clear();
|
||||
SelectAll(selected, tree);
|
||||
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selected one item by DataContext
|
||||
/// </summary>
|
||||
/// <param name="tree"></param>
|
||||
/// <param name="obj"></param>
|
||||
public static void SelectOneByContext(TreeView tree, object obj) {
|
||||
var item = FindTreeViewItemByDataContext(tree, obj);
|
||||
if (item != null) {
|
||||
var selected = GetSelectedItems(tree);
|
||||
selected.Add(item);
|
||||
item.SetValue(IsCheckedProperty, true);
|
||||
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unselect the whole tree.
|
||||
/// </summary>
|
||||
/// <param name="tree"></param>
|
||||
public static void UnselectTree(TreeView tree) {
|
||||
var selected = GetSelectedItems(tree);
|
||||
if (selected.Count == 0) return;
|
||||
|
||||
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
|
||||
selected.Clear();
|
||||
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hooks when EnableMultiSelection changed.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void OnEnableMultiSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
var tree = d as TreeView;
|
||||
if (tree != null && (bool)e.NewValue) {
|
||||
tree.SetValue(SelectedItemsProperty, new ObservableCollection<TreeViewItem>());
|
||||
tree.PreviewMouseDown += OnTreeMouseDown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preview mouse button select.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void OnTreeMouseDown(object sender, MouseButtonEventArgs e) {
|
||||
var tree = sender as TreeView;
|
||||
if (tree == null) return;
|
||||
|
||||
var hit = VisualTreeHelper.HitTest(tree, e.GetPosition(tree));
|
||||
if (hit == null || hit.VisualHit is null) return;
|
||||
|
||||
var item = FindTreeViewItem(hit.VisualHit as UIElement);
|
||||
if (item == null) return;
|
||||
|
||||
var selected = GetSelectedItems(tree);
|
||||
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
|
||||
if (GetIsChecked(item)) {
|
||||
selected.Remove(item);
|
||||
item.SetValue(IsCheckedProperty, false);
|
||||
} else {
|
||||
selected.Add(item);
|
||||
item.SetValue(IsCheckedProperty, true);
|
||||
}
|
||||
} else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && selected.Count > 0) {
|
||||
var last = selected.Last();
|
||||
if (last == item) return;
|
||||
|
||||
var lastPos = last.PointToScreen(new Point(0, 0));
|
||||
var curPos = item.PointToScreen(new Point(0, 0));
|
||||
if (lastPos.Y > curPos.Y) {
|
||||
SelectRange(selected, tree, item, last);
|
||||
} else {
|
||||
SelectRange(selected, tree, last, item);
|
||||
}
|
||||
|
||||
selected.Add(item);
|
||||
item.SetValue(IsCheckedProperty, true);
|
||||
} else if (e.RightButton == MouseButtonState.Pressed) {
|
||||
if (GetIsChecked(item)) return;
|
||||
|
||||
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
|
||||
selected.Clear();
|
||||
selected.Add(item);
|
||||
item.SetValue(IsCheckedProperty, true);
|
||||
} else {
|
||||
if (selected.Count == 1 && selected[0] == item) return;
|
||||
|
||||
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
|
||||
selected.Clear();
|
||||
selected.Add(item);
|
||||
item.SetValue(IsCheckedProperty, true);
|
||||
}
|
||||
|
||||
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find TreeViewItem by child element.
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="child"></param>
|
||||
/// <returns></returns>
|
||||
public static TreeViewItem FindTreeViewItem(DependencyObject child) {
|
||||
if (child == null) return null;
|
||||
if (child is TreeViewItem) return child as TreeViewItem;
|
||||
if (child is TreeView) return null;
|
||||
return FindTreeViewItem(VisualTreeHelper.GetParent(child));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find TreeViewItem by DataContext
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl control, object obj) {
|
||||
if (control == null) return null;
|
||||
if (control.DataContext == obj) return control as TreeViewItem;
|
||||
|
||||
for (int i = 0; i < control.Items.Count; i++) {
|
||||
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
|
||||
var found = FindTreeViewItemByDataContext(child, obj);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select all items.
|
||||
/// </summary>
|
||||
/// <param name="selected"></param>
|
||||
/// <param name="control"></param>
|
||||
private static void SelectAll(ObservableCollection<TreeViewItem> selected, ItemsControl control) {
|
||||
for (int i = 0; i < control.Items.Count; i++) {
|
||||
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
|
||||
if (child == null) continue;
|
||||
|
||||
selected.Add(child);
|
||||
child.SetValue(IsCheckedProperty, true);
|
||||
SelectAll(selected, child);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select range items between given.
|
||||
/// </summary>
|
||||
/// <param name="selected"></param>
|
||||
/// <param name="control"></param>
|
||||
/// <param name="from"></param>
|
||||
/// <param name="to"></param>
|
||||
/// <param name="started"></param>
|
||||
private static int SelectRange(ObservableCollection<TreeViewItem> selected, ItemsControl control, TreeViewItem from, TreeViewItem to, int matches = 0) {
|
||||
for (int i = 0; i < control.Items.Count; i++) {
|
||||
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
|
||||
if (child == null) continue;
|
||||
|
||||
if (matches == 1) {
|
||||
if (child == to) return 2;
|
||||
selected.Add(child);
|
||||
child.SetValue(IsCheckedProperty, true);
|
||||
if (TryEndRangeSelection(selected, child, to)) return 2;
|
||||
} else if (child == from) {
|
||||
matches = 1;
|
||||
if (TryEndRangeSelection(selected, child, to)) return 2;
|
||||
} else {
|
||||
matches = SelectRange(selected, child, from, to, matches);
|
||||
if (matches == 2) return 2;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static bool TryEndRangeSelection(ObservableCollection<TreeViewItem> selected, TreeViewItem control, TreeViewItem end) {
|
||||
for (int i = 0; i < control.Items.Count; i++) {
|
||||
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
|
||||
if (child == null) continue;
|
||||
|
||||
if (child == end) {
|
||||
return true;
|
||||
} else {
|
||||
selected.Add(child);
|
||||
child.SetValue(IsCheckedProperty, true);
|
||||
|
||||
var ended = TryEndRangeSelection(selected, child, end);
|
||||
if (ended) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SourceGit.Helpers {
|
||||
|
||||
/// <summary>
|
||||
/// Validate clone folder.
|
||||
/// </summary>
|
||||
public class CloneFolderRule : ValidationRule {
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var badPath = App.Text("BadCloneFolder");
|
||||
var path = value as string;
|
||||
return Directory.Exists(path) ? ValidationResult.ValidResult : new ValidationResult(false, badPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate git remote URL
|
||||
/// </summary>
|
||||
public class RemoteUriRule : ValidationRule {
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var badUrl = App.Text("BadRemoteUri");
|
||||
return Git.Repository.IsValidUrl(value as string) ? ValidationResult.ValidResult : new ValidationResult(false, badUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate tag name.
|
||||
/// </summary>
|
||||
public class RemoteNameRule : ValidationRule {
|
||||
public Git.Repository Repo { get; set; }
|
||||
public Git.Remote Old { get; set; }
|
||||
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var regex = new Regex(@"^[\w\-\.]+$");
|
||||
var name = value as string;
|
||||
var remotes = Repo.Remotes();
|
||||
|
||||
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, App.Text("EmptyRemoteName"));
|
||||
if (!regex.IsMatch(name)) return new ValidationResult(false, App.Text("BadRemoteName"));
|
||||
|
||||
if (Old == null || name != Old.Name) {
|
||||
foreach (var t in remotes) {
|
||||
if (t.Name == name) {
|
||||
return new ValidationResult(false, App.Text("DuplicatedRemoteName"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.ValidResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate branch name.
|
||||
/// </summary>
|
||||
public class BranchNameRule : ValidationRule {
|
||||
public Git.Repository Repo { get; set; }
|
||||
public string Prefix { get; set; } = "";
|
||||
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var regex = new Regex(@"^[\w\-/\.]+$");
|
||||
var name = value as string;
|
||||
var branches = Repo.Branches();
|
||||
|
||||
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, App.Text("EmptyBranchName"));
|
||||
if (!regex.IsMatch(name)) return new ValidationResult(false, App.Text("BadBranchName"));
|
||||
|
||||
name = Prefix + name;
|
||||
|
||||
foreach (var b in branches) {
|
||||
if (b.Name == name) {
|
||||
return new ValidationResult(false, App.Text("DuplicatedBranchName"));
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.ValidResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate tag name.
|
||||
/// </summary>
|
||||
public class TagNameRule : ValidationRule {
|
||||
public Git.Repository Repo { get; set; }
|
||||
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var regex = new Regex(@"^[\w\-\.]+$");
|
||||
var name = value as string;
|
||||
var tags = Repo.Tags();
|
||||
|
||||
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, App.Text("EmptyTagName"));
|
||||
if (!regex.IsMatch(name)) return new ValidationResult(false, App.Text("BadTagName"));
|
||||
|
||||
foreach (var t in tags) {
|
||||
if (t.Name == name) {
|
||||
return new ValidationResult(false, App.Text("DuplicatedTagName"));
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.ValidResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required for commit subject.
|
||||
/// </summary>
|
||||
public class CommitSubjectRequiredRule : ValidationRule {
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var subject = value as string;
|
||||
return string.IsNullOrWhiteSpace(subject) ? new ValidationResult(false, App.Text("EmptyCommitMessage")) : ValidationResult.ValidResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required for patch file.
|
||||
/// </summary>
|
||||
public class PatchFileRequiredRule : ValidationRule {
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var path = value as string;
|
||||
var succ = !string.IsNullOrEmpty(path) && File.Exists(path);
|
||||
return !succ ? new ValidationResult(false, App.Text("BadPatchFile")) : ValidationResult.ValidResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required for submodule path.
|
||||
/// </summary>
|
||||
public class SubmodulePathRequiredRule : ValidationRule {
|
||||
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
|
||||
var path = value as string;
|
||||
if (string.IsNullOrEmpty(path)) return ValidationResult.ValidResult;
|
||||
|
||||
var regex = new Regex(@"^[\w\-\._/]+$");
|
||||
var succ = regex.IsMatch(path.Trim());
|
||||
return !succ ? new ValidationResult(false, App.Text("BadSubmodulePath")) : ValidationResult.ValidResult;
|
||||
}
|
||||
}
|
||||
}
|
22
src/Models/AvatarServer.cs
Normal file
22
src/Models/AvatarServer.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 支持的头像服务器
|
||||
/// </summary>
|
||||
public class AvatarServer {
|
||||
public string Name { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
public static List<AvatarServer> Supported = new List<AvatarServer>() {
|
||||
new AvatarServer("Gravatar官网", "https://www.gravatar.com/avatar/"),
|
||||
new AvatarServer("Gravatar中国CDN", "https://cdn.s.loli.top/avatar/"),
|
||||
};
|
||||
|
||||
public AvatarServer(string name, string url) {
|
||||
Name = name;
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
}
|
12
src/Models/BlameLine.cs
Normal file
12
src/Models/BlameLine.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
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; }
|
||||
}
|
||||
}
|
16
src/Models/Branch.cs
Normal file
16
src/Models/Branch.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 分支数据
|
||||
/// </summary>
|
||||
public class Branch {
|
||||
public string Name { get; set; }
|
||||
public string FullName { get; set; }
|
||||
public string Head { get; set; }
|
||||
public string HeadSubject { get; set; }
|
||||
public bool IsLocal { get; set; }
|
||||
public bool IsCurrent { get; set; }
|
||||
public string Upstream { get; set; }
|
||||
public string UpstreamTrackStatus { get; set; }
|
||||
public string Remote { get; set; }
|
||||
}
|
||||
}
|
25
src/Models/CRLFOption.cs
Normal file
25
src/Models/CRLFOption.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
74
src/Models/Change.cs
Normal file
74
src/Models/Change.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
namespace SourceGit.Models {
|
||||
|
||||
/// <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 string Path { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
|
||||
public bool IsAddedToIndex {
|
||||
get {
|
||||
if (Index == Status.None || Index == Status.Untracked) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Set(Status index, Status workTree = Status.None) {
|
||||
Index = index;
|
||||
WorkTree = workTree;
|
||||
|
||||
if (index == Status.Renamed || workTree == Status.Renamed) {
|
||||
var idx = Path.IndexOf('\t');
|
||||
if (idx >= 0) {
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
Path = Path.Substring(idx + 1);
|
||||
} else {
|
||||
idx = Path.IndexOf(" -> ");
|
||||
if (idx > 0) {
|
||||
OriginalPath = Path.Substring(0, idx);
|
||||
Path = Path.Substring(idx + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Path[0] == '"') Path = Path.Substring(1, Path.Length - 2);
|
||||
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
|
||||
}
|
||||
}
|
||||
}
|
21
src/Models/Commit.cs
Normal file
21
src/Models/Commit.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 提交记录
|
||||
/// </summary>
|
||||
public class Commit {
|
||||
public string SHA { get; set; } = "";
|
||||
public string ShortSHA => SHA.Substring(0, 8);
|
||||
public User Author { get; set; } = new User();
|
||||
public User Committer { get; set; } = new User();
|
||||
public string Subject { get; set; } = "";
|
||||
public string Message { get; set; } = "";
|
||||
public List<string> Parents { get; set; } = new List<string>();
|
||||
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
|
||||
public bool HasDecorators => Decorators.Count > 0;
|
||||
public bool IsMerged { get; set; } = false;
|
||||
public Thickness Margin { get; set; } = new Thickness(0);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
namespace SourceGit.Git {
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// Decorator type.
|
||||
/// 修饰类型
|
||||
/// </summary>
|
||||
public enum DecoratorType {
|
||||
None,
|
||||
|
@ -12,10 +12,10 @@
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit decorator.
|
||||
/// 提交的附加修饰
|
||||
/// </summary>
|
||||
public class Decorator {
|
||||
public DecoratorType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DecoratorType Type { get; set; } = DecoratorType.None;
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
}
|
15
src/Models/Exception.cs
Normal file
15
src/Models/Exception.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 错误通知
|
||||
/// </summary>
|
||||
public static class Exception {
|
||||
public static Action<string> Handler { get; set; }
|
||||
|
||||
public static void Raise(string error) {
|
||||
Handler?.Invoke(error);
|
||||
}
|
||||
}
|
||||
}
|
9
src/Models/FileSizeChange.cs
Normal file
9
src/Models/FileSizeChange.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 文件大小变化
|
||||
/// </summary>
|
||||
public class FileSizeChange {
|
||||
public long OldSize = 0;
|
||||
public long NewSize = 0;
|
||||
}
|
||||
}
|
36
src/Models/GitFlow.cs
Normal file
36
src/Models/GitFlow.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// GitFlow的分支类型
|
||||
/// </summary>
|
||||
public enum GitFlowBranchType {
|
||||
None,
|
||||
Feature,
|
||||
Release,
|
||||
Hotfix,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GitFlow相关设置
|
||||
/// </summary>
|
||||
public class GitFlow {
|
||||
public string Feature { get; set; }
|
||||
public string Release { get; set; }
|
||||
public string Hotfix { get; set; }
|
||||
|
||||
public bool IsEnabled {
|
||||
get {
|
||||
return !string.IsNullOrEmpty(Feature)
|
||||
&& !string.IsNullOrEmpty(Release)
|
||||
&& !string.IsNullOrEmpty(Hotfix);
|
||||
}
|
||||
}
|
||||
|
||||
public GitFlowBranchType GetBranchType(string name) {
|
||||
if (!IsEnabled) return GitFlowBranchType.None;
|
||||
if (name.StartsWith(Feature)) return GitFlowBranchType.Feature;
|
||||
if (name.StartsWith(Release)) return GitFlowBranchType.Release;
|
||||
if (name.StartsWith(Hotfix)) return GitFlowBranchType.Hotfix;
|
||||
return GitFlowBranchType.None;
|
||||
}
|
||||
}
|
||||
}
|
12
src/Models/Group.cs
Normal file
12
src/Models/Group.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 仓库列表分组
|
||||
/// </summary>
|
||||
public class Group {
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Parent { get; set; } = "";
|
||||
public bool IsExpanded { get; set; } = false;
|
||||
}
|
||||
}
|
10
src/Models/LFSChange.cs
Normal file
10
src/Models/LFSChange.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// LFS对象变更
|
||||
/// </summary>
|
||||
public class LFSChange {
|
||||
public LFSObject Old;
|
||||
public LFSObject New;
|
||||
public bool IsValid => Old != null || New != null;
|
||||
}
|
||||
}
|
9
src/Models/LFSObject.cs
Normal file
9
src/Models/LFSObject.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// LFS对象
|
||||
/// </summary>
|
||||
public class LFSObject {
|
||||
public string OID { get; set; }
|
||||
public long Size { get; set; }
|
||||
}
|
||||
}
|
22
src/Models/Locale.cs
Normal file
22
src/Models/Locale.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
25
src/Models/MergeOption.cs
Normal file
25
src/Models/MergeOption.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
119
src/Models/MergeTool.cs
Normal file
119
src/Models/MergeTool.cs
Normal file
|
@ -0,0 +1,119 @@
|
|||
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 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\"", FindVSCode),
|
||||
new MergeTool(2, "Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge),
|
||||
new MergeTool(3, "Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge),
|
||||
new MergeTool(4, "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3),
|
||||
new MergeTool(5, "Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare),
|
||||
};
|
||||
|
||||
public MergeTool(int type, string name, string exec, string cmd, Func<string> finder) {
|
||||
Type = type;
|
||||
Name = name;
|
||||
Exec = exec;
|
||||
Cmd = cmd;
|
||||
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 tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN");
|
||||
if (tortoiseSVN == null) return "";
|
||||
return tortoiseSVN.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";
|
||||
}
|
||||
}
|
||||
}
|
21
src/Models/Object.cs
Normal file
21
src/Models/Object.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace SourceGit.Models {
|
||||
/// <summary>
|
||||
/// 提交中元素类型
|
||||
/// </summary>
|
||||
public enum ObjectType {
|
||||
None,
|
||||
Blob,
|
||||
Tree,
|
||||
Tag,
|
||||
Commit,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Git提交中的元素
|
||||
/// </summary>
|
||||
public class Object {
|
||||
public string SHA { get; set; }
|
||||
public ObjectType Type { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
}
|
314
src/Models/Preference.cs
Normal file
314
src/Models/Preference.cs
Normal file
|
@ -0,0 +1,314 @@
|
|||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
#if NET48
|
||||
using Newtonsoft.Json;
|
||||
#else
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
#endif
|
||||
|
||||
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 class GeneralInfo {
|
||||
|
||||
/// <summary>
|
||||
/// 显示语言
|
||||
/// </summary>
|
||||
public string Locale { get; set; } = "en_US";
|
||||
|
||||
/// <summary>
|
||||
/// 头像服务器
|
||||
/// </summary>
|
||||
public string AvatarServer { get; set; } = "https://www.gravatar.com/avatar/";
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用深色主题
|
||||
/// </summary>
|
||||
public bool UseDarkTheme { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 启用更新检测
|
||||
/// </summary>
|
||||
public bool CheckForUpdate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 上一次检测的时间(用于控制每天仅第一次启动软件时,检测)
|
||||
/// </summary>
|
||||
public int LastCheckDay { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 启用自动拉取远程变更(每10分钟一次)
|
||||
/// </summary>
|
||||
public bool AutoFetchRemotes { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Git配置
|
||||
/// </summary>
|
||||
public class GitInfo {
|
||||
|
||||
/// <summary>
|
||||
/// git.exe所在路径
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认克隆路径
|
||||
/// </summary>
|
||||
public string DefaultCloneDir { get; set; }
|
||||
}
|
||||
|
||||
/// <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 bool MoveCommitInfoRight { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 使用合并Diff视图
|
||||
/// </summary>
|
||||
public bool UseCombinedDiff { get; set; } = false;
|
||||
|
||||
/// <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>
|
||||
[JsonIgnore]
|
||||
public static Preference Instance {
|
||||
get {
|
||||
if (instance == null) return Load();
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测配置是否
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsReady {
|
||||
get {
|
||||
return !string.IsNullOrEmpty(Git.Path) && File.Exists(Git.Path);
|
||||
}
|
||||
}
|
||||
|
||||
#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<Group> Groups { get; set; } = new List<Group>();
|
||||
public List<Repository> Repositories { get; set; } = new List<Repository>();
|
||||
#endregion
|
||||
|
||||
#region LOAD_SAVE
|
||||
public static Preference Load() {
|
||||
if (!File.Exists(SAVE_PATH)) {
|
||||
instance = new Preference();
|
||||
} else {
|
||||
#if NET48
|
||||
instance = JsonConvert.DeserializeObject<Preference>(File.ReadAllText(SAVE_PATH));
|
||||
#else
|
||||
instance = JsonSerializer.Deserialize<Preference>(File.ReadAllText(SAVE_PATH));
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
#if NET48
|
||||
var data = JsonConvert.SerializeObject(instance, Formatting.Indented);
|
||||
#else
|
||||
var data = JsonSerializer.Serialize(instance, new JsonSerializerOptions() { WriteIndented = true });
|
||||
#endif
|
||||
File.WriteAllText(SAVE_PATH, data);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region METHOD_ON_GROUPS
|
||||
public Group AddGroup(string name, string parentId) {
|
||||
var group = new Group() {
|
||||
Name = name,
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Parent = parentId,
|
||||
IsExpanded = false,
|
||||
};
|
||||
|
||||
Groups.Add(group);
|
||||
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
public Group FindGroup(string id) {
|
||||
foreach (var group in Groups) {
|
||||
if (group.Id == id) return group;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RenameGroup(string id, string newName) {
|
||||
foreach (var group in Groups) {
|
||||
if (group.Id == id) {
|
||||
group.Name = newName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
}
|
||||
|
||||
public void RemoveGroup(string id) {
|
||||
int removedIdx = -1;
|
||||
|
||||
for (int i = 0; i < Groups.Count; i++) {
|
||||
if (Groups[i].Id == id) {
|
||||
removedIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedIdx >= 0) Groups.RemoveAt(removedIdx);
|
||||
}
|
||||
|
||||
public bool IsSubGroup(string parent, string subId) {
|
||||
if (string.IsNullOrEmpty(parent)) return false;
|
||||
if (parent == subId) return true;
|
||||
|
||||
var g = FindGroup(subId);
|
||||
if (g == null) return false;
|
||||
|
||||
g = FindGroup(g.Parent);
|
||||
while (g != null) {
|
||||
if (g.Id == parent) return true;
|
||||
g = FindGroup(g.Parent);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region METHOD_ON_REPOSITORIES
|
||||
public Repository AddRepository(string path, string gitDir, string groupId) {
|
||||
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,
|
||||
GroupId = groupId,
|
||||
};
|
||||
|
||||
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 RenameRepository(string path, string newName) {
|
||||
var repo = FindRepository(path);
|
||||
if (repo == null) return;
|
||||
|
||||
repo.Name = newName;
|
||||
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
10
src/Models/Remote.cs
Normal file
10
src/Models/Remote.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 远程
|
||||
/// </summary>
|
||||
public class Remote {
|
||||
public string Name { get; set; }
|
||||
public string URL { get; set; }
|
||||
}
|
||||
}
|
48
src/Models/Repository.cs
Normal file
48
src/Models/Repository.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
#if NET48
|
||||
using Newtonsoft.Json;
|
||||
#else
|
||||
using System.Text.Json.Serialization;
|
||||
#endif
|
||||
|
||||
namespace SourceGit.Models {
|
||||
|
||||
/// <summary>
|
||||
/// 仓库
|
||||
/// </summary>
|
||||
public class Repository {
|
||||
|
||||
#region PROPERTIES_SAVED
|
||||
public string Name { get; set; } = "";
|
||||
public string Path { get; set; } = "";
|
||||
public string GitDir { get; set; } = "";
|
||||
public string GroupId { get; set; } = "";
|
||||
public int Bookmark { get; set; } = 0;
|
||||
public List<string> Filters { get; set; } = new List<string>();
|
||||
public List<string> CommitMessages { get; set; } = new List<string>();
|
||||
#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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
27
src/Models/ResetMode.cs
Normal file
27
src/Models/ResetMode.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
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.Yellow),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue