refactor<*>: rewrite all codes...

This commit is contained in:
leo 2021-04-29 20:05:55 +08:00
parent 89ff8aa744
commit 30ab8ae954
342 changed files with 17208 additions and 19633 deletions

View file

@ -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 已内置该运行时
## 预览

View file

@ -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 ../

View file

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

View file

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

View file

@ -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,211 +10,106 @@ 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();
// 如果启动命令中指定了路径,打开指定目录的仓库
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, "");
}
}
// Check for update.
if (Setting.CheckUpdate && Setting.LastCheckUpdate != DateTime.Now.DayOfYear) {
Setting.LastCheckUpdate = DateTime.Now.DayOfYear;
SaveSetting();
Task.Run(CheckUpdate);
if (repo != null) Models.Watcher.Open(repo);
}
// 检测更新
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 = Models.Version.Load(raw);
var cur = Assembly.GetExecutingAssembly().GetName().Version;
var matches = Regex.Match(ver.TagName, @"^v(\d+)\.(\d+).*");
if (!matches.Success) return;
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(() => Views.Upgrade.Open(MainWindow, ver));
}
} catch {}
});
}
}
}
/// <summary>
/// Deactivated event.
/// 后台运行
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppDeactivated(object sender, EventArgs e) {
GC.Collect();
SaveSetting();
}
/// <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() {
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 cur = Assembly.GetExecutingAssembly().GetName().Version;
var matches = Regex.Match(ver.TagName, @"^v(\d+)\.(\d+).*");
if (!matches.Success) return;
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();
});
}
} catch {
// IGNORE
}
Models.Preference.Save();
}
}
}

27
src/Commands/Add.cs Normal file
View 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
View 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
View 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
View 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
View 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
View 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();
}
}
}

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

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

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

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

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

@ -0,0 +1,13 @@
namespace SourceGit.Commands {
/// <summary>
/// 初始化Git仓库
/// </summary>
public class Init : Command {
public Init(string workDir) {
Cwd = workDir;
Args = "init -q";
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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