diff --git a/README.md b/README.md
index 72c1500a..597efba8 100644
--- a/README.md
+++ b/README.md
@@ -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 已内置该运行时
## 预览
diff --git a/build.bat b/build.bat
index d039bd6f..d973a67a 100644
--- a/build.bat
+++ b/build.bat
@@ -1,9 +1,16 @@
+rmdir /s /q publish
+
cd src
rmdir /s /q bin
rmdir /s /q obj
-dotnet publish --nologo -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained -o ../publish
+dotnet publish SourceGit.csproj --nologo -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained -o ../publish
rmdir /s /q bin
rmdir /s /q obj
-dotnet publish --nologo -c Release -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained -o ../publish/SourceGit
-cd ..
\ No newline at end of file
+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 ../
\ No newline at end of file
diff --git a/src/App.preference.cs b/src/App.preference.cs
deleted file mode 100644
index a30204a1..00000000
--- a/src/App.preference.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-
-namespace SourceGit {
-
- ///
- /// User's preference settings. Serialized to
- ///
- public class Preference {
-
- ///
- /// Tools setting.
- ///
- public class ToolSetting {
- ///
- /// Git executable file path.
- ///
- public string GitExecutable { get; set; }
- ///
- /// Default clone directory.
- ///
- public string GitDefaultCloneDir { get; set; }
- ///
- /// Selected merge tool.
- ///
- public int MergeTool { get; set; } = 0;
- ///
- /// Executable file path for merge tool.
- ///
- public string MergeExecutable { get; set; } = "--";
- }
-
- ///
- /// File's display mode.
- ///
- public enum FilesDisplayMode {
- Tree,
- List,
- Grid,
- }
-
- ///
- /// Settings for UI.
- ///
- public class UISetting {
- ///
- /// Use light theme?
- ///
- public bool UseLightTheme { get; set; }
- ///
- /// Locale
- ///
- public string Locale { get; set; } = "en_US";
- ///
- /// Base URL to get avatar
- ///
- public string AvatarServer { get; set; } = "https://www.gravatar.com/avatar/";
- ///
- /// Main window width
- ///
- public double WindowWidth { get; set; }
- ///
- /// Main window height
- ///
- public double WindowHeight { get; set; }
- ///
- /// Move commit viewer from bottom to right
- ///
- public bool MoveCommitViewerRight { get; set; }
- ///
- /// File's display mode in unstaged view.
- ///
- public FilesDisplayMode UnstageFileDisplayMode { get; set; }
- ///
- /// File's display mode in staged view.
- ///
- public FilesDisplayMode StagedFileDisplayMode { get; set; }
- ///
- /// Use DataGrid instead of TreeView in changes view.
- ///
- public bool UseListInChanges { get; set; }
- ///
- /// Use combined instead of side-by-side mode in diff viewer.
- ///
- public bool UseCombinedDiff { get; set; }
- }
-
- ///
- /// Group(Virtual folder) for watched repositories.
- ///
- public class Group {
- ///
- /// Unique ID of this group.
- ///
- public string Id { get; set; }
- ///
- /// Display name.
- ///
- public string Name { get; set; }
- ///
- /// Parent ID.
- ///
- public string ParentId { get; set; }
- ///
- /// Cache UI IsExpended status.
- ///
- public bool IsExpended { get; set; }
- }
-
- #region SAVED_DATAS
- ///
- /// Check for updates.
- ///
- public bool CheckUpdate { get; set; } = true;
- ///
- /// Last UNIX timestamp to check for update.
- ///
- public int LastCheckUpdate { get; set; } = 0;
- ///
- /// Fetch remotes automatically?
- ///
- public bool AutoFetchRemotes { get; set; } = true;
- ///
- /// Settings for executables.
- ///
- public ToolSetting Tools { get; set; } = new ToolSetting();
- ///
- /// Use light color theme.
- ///
- public UISetting UI { get; set; } = new UISetting();
- #endregion
-
- #region SETTING_REPOS
- ///
- /// Groups for repositories.
- ///
- public List Groups { get; set; } = new List();
- ///
- /// Watched repositories.
- ///
- public List Repositories { get; set; } = new List();
- #endregion
-
- #region METHODS_ON_GROUP
- ///
- /// Add new group(virtual folder).
- ///
- /// Display name.
- /// Parent group ID.
- /// Added group instance.
- 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;
- }
-
- ///
- /// Find group by ID.
- ///
- /// Unique ID
- /// Founded group's instance.
- public Group FindGroup(string id) {
- foreach (var group in Groups) {
- if (group.Id == id) return group;
- }
- return null;
- }
-
- ///
- /// Rename group.
- ///
- /// Unique ID
- /// New name.
- 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));
- }
-
- ///
- /// Remove a group.
- ///
- /// Unique ID
- 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);
- }
-
- ///
- /// Check if given group has relations.
- ///
- ///
- ///
- ///
- 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
- ///
- /// Add repository.
- ///
- /// Local storage path.
- /// Group's ID
- /// Added repository instance.
- 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;
- }
-
- ///
- /// Find repository by path.
- ///
- /// Local storage path.
- /// Founded repository instance.
- 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;
- }
-
- ///
- /// Change a repository's display name in RepositoryManager.
- ///
- /// Local storage path.
- /// New name
- 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));
- }
-
- ///
- /// Remove a repository in RepositoryManager.
- ///
- /// Local storage path.
- 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
- }
-}
diff --git a/src/App.xaml b/src/App.xaml
index a85e0cf5..3eb35fa1 100644
--- a/src/App.xaml
+++ b/src/App.xaml
@@ -8,7 +8,7 @@
-
+
diff --git a/src/App.xaml.cs b/src/App.xaml.cs
index a92df019..94a8780b 100644
--- a/src/App.xaml.cs
+++ b/src/App.xaml.cs
@@ -1,10 +1,8 @@
-using Microsoft.Win32;
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
-using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
@@ -12,211 +10,106 @@ using System.Windows;
namespace SourceGit {
///
- /// Application.
+ /// 程序入口.
///
public partial class App : Application {
- ///
- /// Getter/Setter for application user setting.
- ///
- public static Preference Setting { get; set; }
///
- /// Check if GIT has been configured.
+ /// 读取本地化字串
///
- public static bool IsGitConfigured {
- get {
- return !string.IsNullOrEmpty(Setting.Tools.GitExecutable)
- && File.Exists(Setting.Tools.GitExecutable);
- }
+ /// 本地化字串的Key
+ /// 可选格式化参数
+ /// 本地化字串
+ 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);
}
///
- /// Load text from locales.
- ///
- ///
- ///
- public static string Text(string key) {
- return Current.FindResource("Text." + key) as string;
- }
-
- ///
- /// Format text
- ///
- ///
- ///
- ///
- public static string Format(string key, params object[] args) {
- return string.Format(Text(key), args);
- }
-
- ///
- /// Raise error message.
- ///
- ///
- public static void RaiseError(string msg) {
- Current.Dispatcher.Invoke(() => {
- (Current.MainWindow as UI.Launcher).Errors.Add(msg);
- });
- }
-
- ///
- /// Open repository.
- ///
- ///
- public static void Open(Git.Repository repo) {
- (Current.MainWindow as UI.Launcher).Open(repo);
- }
-
- ///
- /// Save settings.
- ///
- 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);
- }
-
- ///
- /// Startup event.
+ /// 启动.
///
///
///
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(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 {}
+ });
+ }
}
}
///
- /// Deactivated event.
+ /// 后台运行
///
///
///
private void OnAppDeactivated(object sender, EventArgs e) {
- GC.Collect();
- SaveSetting();
- }
-
- ///
- /// Try to open app as git editor
- ///
- ///
- ///
- 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;
- }
-
- ///
- /// Check for update.
- ///
- 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(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();
}
}
}
diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs
new file mode 100644
index 00000000..fb98cf87
--- /dev/null
+++ b/src/Commands/Add.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace SourceGit.Commands {
+ ///
+ /// `git add`命令
+ ///
+ public class Add : Command {
+ public Add(string repo) {
+ Cwd = repo;
+ Args = "add .";
+ }
+
+ public Add(string repo, List 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();
+ }
+ }
+}
diff --git a/src/Commands/Apply.cs b/src/Commands/Apply.cs
new file mode 100644
index 00000000..f62f113c
--- /dev/null
+++ b/src/Commands/Apply.cs
@@ -0,0 +1,15 @@
+namespace SourceGit.Commands {
+ ///
+ /// 应用Patch
+ ///
+ 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}\"";
+ }
+ }
+}
diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs
new file mode 100644
index 00000000..eede5927
--- /dev/null
+++ b/src/Commands/Blame.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 逐行追溯
+ ///
+ 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 Lines = new List();
+ 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);
+ }
+ }
+}
diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs
new file mode 100644
index 00000000..7f640c4f
--- /dev/null
+++ b/src/Commands/Branch.cs
@@ -0,0 +1,33 @@
+namespace SourceGit.Commands {
+ ///
+ /// 分支相关操作
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/Commands/Branches.cs b/src/Commands/Branches.cs
new file mode 100644
index 00000000..687bb151
--- /dev/null
+++ b/src/Commands/Branches.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 解析所有的分支
+ ///
+ 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 loaded = new List();
+
+ public Branches(string path) {
+ Cwd = path;
+ Args = CMD;
+ }
+
+ public List 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();
+ }
+ }
+}
diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs
new file mode 100644
index 00000000..f3d976da
--- /dev/null
+++ b/src/Commands/Checkout.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace SourceGit.Commands {
+ ///
+ /// 检出
+ ///
+ 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 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();
+ }
+ }
+}
diff --git a/src/Commands/CherryPick.cs b/src/Commands/CherryPick.cs
new file mode 100644
index 00000000..ca939e76
--- /dev/null
+++ b/src/Commands/CherryPick.cs
@@ -0,0 +1,13 @@
+namespace SourceGit.Commands {
+ ///
+ /// 遴选命令
+ ///
+ public class CherryPick : Command {
+
+ public CherryPick(string repo, string commit, bool noCommit) {
+ var mode = noCommit ? "-n" : "--ff";
+ Cwd = repo;
+ Args = $"cherry-pick {mode} {commit}";
+ }
+ }
+}
diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs
new file mode 100644
index 00000000..83de46cd
--- /dev/null
+++ b/src/Commands/Clean.cs
@@ -0,0 +1,12 @@
+namespace SourceGit.Commands {
+ ///
+ /// 清理指令
+ ///
+ public class Clean : Command {
+
+ public Clean(string repo) {
+ Cwd = repo;
+ Args = "clean -qfd";
+ }
+ }
+}
diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs
new file mode 100644
index 00000000..d4b3f2fc
--- /dev/null
+++ b/src/Commands/Clone.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 克隆
+ ///
+ public class Clone : Command {
+ private Action handler = null;
+
+ public Clone(string path, string url, string localName, string extraArgs, Action 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);
+ }
+ }
+}
diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs
new file mode 100644
index 00000000..adf87876
--- /dev/null
+++ b/src/Commands/Command.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 取消命令执行的对象
+ ///
+ public class Cancellable {
+ public bool IsCancelRequested { get; set; } = false;
+ }
+
+ ///
+ /// 命令接口
+ ///
+ public class Command {
+
+ ///
+ /// 读取全部输出时的结果
+ ///
+ public class ReadToEndResult {
+ public bool IsSuccess { get; set; }
+ public string Output { get; set; }
+ public string Error { get; set; }
+ }
+
+ ///
+ /// 运行路径
+ ///
+ public string Cwd { get; set; } = "";
+
+ ///
+ /// 参数
+ ///
+ public string Args { get; set; } = "";
+
+ ///
+ /// 使用标准错误输出
+ ///
+ public bool TraitErrorAsOutput { get; set; } = false;
+
+ ///
+ /// 用于取消命令指行的Token
+ ///
+ public Cancellable Token { get; set; } = null;
+
+ ///
+ /// 运行
+ ///
+ 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();
+ 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;
+ }
+ }
+
+ ///
+ /// 直接读取全部标准输出
+ ///
+ 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;
+ }
+
+ ///
+ /// 调用Exec时的读取函数
+ ///
+ ///
+ public virtual void OnReadline(string line) {}
+ }
+}
diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs
new file mode 100644
index 00000000..4a5f2471
--- /dev/null
+++ b/src/Commands/Commit.cs
@@ -0,0 +1,19 @@
+using System.IO;
+
+namespace SourceGit.Commands {
+ ///
+ /// `git commit`命令
+ ///
+ 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";
+ }
+ }
+}
diff --git a/src/Commands/CommitChanges.cs b/src/Commands/CommitChanges.cs
new file mode 100644
index 00000000..defcbff2
--- /dev/null
+++ b/src/Commands/CommitChanges.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 取得一个提交的变更列表
+ ///
+ public class CommitChanges : Command {
+ private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
+ private List changes = new List();
+
+ public CommitChanges(string cwd, string commit) {
+ Cwd = cwd;
+ Args = $"show --name-status {commit}";
+ }
+
+ public List 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;
+ }
+ }
+ }
+}
diff --git a/src/Commands/CommitRangeChanges.cs b/src/Commands/CommitRangeChanges.cs
new file mode 100644
index 00000000..05cc778e
--- /dev/null
+++ b/src/Commands/CommitRangeChanges.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 对比两个提交间的变更
+ ///
+ public class CommitRangeChanges : Command {
+ private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
+ private List changes = new List();
+
+ public CommitRangeChanges(string cwd, string start, string end) {
+ Cwd = cwd;
+ Args = $"diff --name-status {start} {end}";
+ }
+
+ public List 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;
+ }
+ }
+ }
+}
diff --git a/src/Commands/Commits.cs b/src/Commands/Commits.cs
new file mode 100644
index 00000000..e5839e5c
--- /dev/null
+++ b/src/Commands/Commits.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 取得提交列表
+ ///
+ 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 commits = new List();
+ 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 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 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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs
new file mode 100644
index 00000000..7b99a99d
--- /dev/null
+++ b/src/Commands/Config.cs
@@ -0,0 +1,37 @@
+namespace SourceGit.Commands {
+ ///
+ /// config命令
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs
new file mode 100644
index 00000000..6679279f
--- /dev/null
+++ b/src/Commands/Diff.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// Diff命令(用于文件文件比对)
+ ///
+ 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++;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs
new file mode 100644
index 00000000..42b4005e
--- /dev/null
+++ b/src/Commands/Discard.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+
+namespace SourceGit.Commands {
+ ///
+ /// 忽略变更
+ ///
+ public class Discard {
+ private string repo = null;
+ private List files = new List();
+
+ public Discard(string repo, List 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;
+ }
+ }
+}
diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs
new file mode 100644
index 00000000..2eeeacee
--- /dev/null
+++ b/src/Commands/Fetch.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 拉取
+ ///
+ public class Fetch : Command {
+ private Action handler = null;
+
+ public Fetch(string repo, string remote, bool prune, Action 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);
+ }
+ }
+
+ ///
+ /// 自动拉取(每隔10分钟)
+ ///
+ public class AutoFetch {
+ private static Dictionary jobs = new Dictionary();
+
+ 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);
+ }
+ }
+}
diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs
new file mode 100644
index 00000000..af5fb624
--- /dev/null
+++ b/src/Commands/FormatPatch.cs
@@ -0,0 +1,12 @@
+namespace SourceGit.Commands {
+ ///
+ /// 将Commit另存为Patch文件
+ ///
+ public class FormatPatch : Command {
+
+ public FormatPatch(string repo, string commit, string path) {
+ Cwd = repo;
+ Args = $"format-patch {commit} -1 -o \"{path}\"";
+ }
+ }
+}
diff --git a/src/Commands/GetRepositoryRootPath.cs b/src/Commands/GetRepositoryRootPath.cs
new file mode 100644
index 00000000..c4dc6777
--- /dev/null
+++ b/src/Commands/GetRepositoryRootPath.cs
@@ -0,0 +1,17 @@
+namespace SourceGit.Commands {
+ ///
+ /// 取得一个库的根路径
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs
new file mode 100644
index 00000000..b33b623c
--- /dev/null
+++ b/src/Commands/GitFlow.cs
@@ -0,0 +1,71 @@
+namespace SourceGit.Commands {
+ ///
+ /// Git-Flow命令
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/Commands/Init.cs b/src/Commands/Init.cs
new file mode 100644
index 00000000..35dde5a2
--- /dev/null
+++ b/src/Commands/Init.cs
@@ -0,0 +1,13 @@
+namespace SourceGit.Commands {
+
+ ///
+ /// 初始化Git仓库
+ ///
+ public class Init : Command {
+
+ public Init(string workDir) {
+ Cwd = workDir;
+ Args = "init -q";
+ }
+ }
+}
diff --git a/src/Commands/IsBinaryFile.cs b/src/Commands/IsBinaryFile.cs
new file mode 100644
index 00000000..68cbff62
--- /dev/null
+++ b/src/Commands/IsBinaryFile.cs
@@ -0,0 +1,18 @@
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 查询指定版本下的某文件是否是二进制文件
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Commands/IsLFSFiltered.cs b/src/Commands/IsLFSFiltered.cs
new file mode 100644
index 00000000..65a4f1f1
--- /dev/null
+++ b/src/Commands/IsLFSFiltered.cs
@@ -0,0 +1,16 @@
+namespace SourceGit.Commands {
+ ///
+ /// 检测目录是否被LFS管理
+ ///
+ 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");
+ }
+ }
+}
diff --git a/src/Commands/LocalChanges.cs b/src/Commands/LocalChanges.cs
new file mode 100644
index 00000000..68f3e8f8
--- /dev/null
+++ b/src/Commands/LocalChanges.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 取得本地工作副本变更
+ ///
+ public class LocalChanges : Command {
+ private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
+ private List changes = new List();
+
+ public LocalChanges(string path) {
+ Cwd = path;
+ Args = "status -uall --ignore-submodules=dirty --porcelain";
+ }
+
+ public List 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);
+ }
+ }
+}
diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs
new file mode 100644
index 00000000..ee827390
--- /dev/null
+++ b/src/Commands/Merge.cs
@@ -0,0 +1,12 @@
+namespace SourceGit.Commands {
+ ///
+ /// 合并分支
+ ///
+ public class Merge : Command {
+
+ public Merge(string repo, string source, string mode) {
+ Cwd = repo;
+ Args = $"merge {source} {mode}";
+ }
+ }
+}
diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs
new file mode 100644
index 00000000..99eaa86b
--- /dev/null
+++ b/src/Commands/Pull.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 拉回
+ ///
+ public class Pull : Command {
+ private Action handler = null;
+ private bool needStash = false;
+
+ public Pull(string repo, string remote, string branch, bool useRebase, bool autoStash, Action 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);
+ }
+ }
+}
diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs
new file mode 100644
index 00000000..2beafcb7
--- /dev/null
+++ b/src/Commands/Push.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace SourceGit.Commands {
+ ///
+ /// 推送
+ ///
+ public class Push : Command {
+ private Action handler = null;
+
+ public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action 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);
+ }
+ }
+}
diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs
new file mode 100644
index 00000000..04eab006
--- /dev/null
+++ b/src/Commands/QueryFileContent.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace SourceGit.Commands {
+ ///
+ /// 取得指定提交下的某文件内容
+ ///
+ public class QueryFileContent : Command {
+ private List lines = new List();
+ private int added = 0;
+
+ public QueryFileContent(string repo, string commit, string path) {
+ Cwd = repo;
+ Args = $"show {commit}:\"{path}\"";
+ }
+
+ public List Result() {
+ Exec();
+ return lines;
+ }
+
+ public override void OnReadline(string line) {
+ added++;
+ lines.Add(new Models.TextLine() { Number = added, Data = line });
+ }
+ }
+}
diff --git a/src/Commands/QueryFileSizeChange.cs b/src/Commands/QueryFileSizeChange.cs
new file mode 100644
index 00000000..f17b2fd4
--- /dev/null
+++ b/src/Commands/QueryFileSizeChange.cs
@@ -0,0 +1,50 @@
+using System.IO;
+
+namespace SourceGit.Commands {
+ ///
+ /// 查询文件大小变化
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Commands/QueryGitDir.cs b/src/Commands/QueryGitDir.cs
new file mode 100644
index 00000000..dd421a21
--- /dev/null
+++ b/src/Commands/QueryGitDir.cs
@@ -0,0 +1,23 @@
+using System.IO;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 取得GitDir
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Commands/QueryLFSObject.cs b/src/Commands/QueryLFSObject.cs
new file mode 100644
index 00000000..8db8bbe0
--- /dev/null
+++ b/src/Commands/QueryLFSObject.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace SourceGit.Commands {
+ ///
+ /// 取得一个LFS对象的信息
+ ///
+ 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());
+ }
+ }
+ }
+}
diff --git a/src/Commands/QueryLFSObjectChange.cs b/src/Commands/QueryLFSObjectChange.cs
new file mode 100644
index 00000000..5f9e1631
--- /dev/null
+++ b/src/Commands/QueryLFSObjectChange.cs
@@ -0,0 +1,41 @@
+namespace SourceGit.Commands {
+ ///
+ /// 查询LFS对象变更
+ ///
+ 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));
+ }
+ }
+ }
+}
diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs
new file mode 100644
index 00000000..791233af
--- /dev/null
+++ b/src/Commands/Rebase.cs
@@ -0,0 +1,14 @@
+namespace SourceGit.Commands {
+ ///
+ /// 变基命令
+ ///
+ public class Rebase : Command {
+
+ public Rebase(string repo, string basedOn, bool autoStash) {
+ Cwd = repo;
+ Args = "rebase ";
+ if (autoStash) Args += "--autostash ";
+ Args += basedOn;
+ }
+ }
+}
diff --git a/src/Commands/Remote.cs b/src/Commands/Remote.cs
new file mode 100644
index 00000000..7bbc73ad
--- /dev/null
+++ b/src/Commands/Remote.cs
@@ -0,0 +1,31 @@
+namespace SourceGit.Commands {
+ ///
+ /// 远程操作
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/Commands/Remotes.cs b/src/Commands/Remotes.cs
new file mode 100644
index 00000000..1866b7f2
--- /dev/null
+++ b/src/Commands/Remotes.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 获取远程列表
+ ///
+ public class Remotes : Command {
+ private static readonly Regex REG_REMOTE = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
+ private List loaded = new List();
+
+ public Remotes(string repo) {
+ Cwd = repo;
+ Args = "remote -v";
+ }
+
+ public List 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);
+ }
+ }
+}
diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs
new file mode 100644
index 00000000..b60ca174
--- /dev/null
+++ b/src/Commands/Reset.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace SourceGit.Commands {
+ ///
+ /// 重置命令
+ ///
+ 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 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();
+ }
+ }
+}
diff --git a/src/Commands/Revert.cs b/src/Commands/Revert.cs
new file mode 100644
index 00000000..2a656fc8
--- /dev/null
+++ b/src/Commands/Revert.cs
@@ -0,0 +1,13 @@
+namespace SourceGit.Commands {
+ ///
+ /// 撤销提交
+ ///
+ public class Revert : Command {
+
+ public Revert(string repo, string commit, bool autoCommit) {
+ Cwd = repo;
+ Args = $"revert {commit} --no-edit";
+ if (!autoCommit) Args += " --no-commit";
+ }
+ }
+}
diff --git a/src/Commands/RevisionObjects.cs b/src/Commands/RevisionObjects.cs
new file mode 100644
index 00000000..25a190ec
--- /dev/null
+++ b/src/Commands/RevisionObjects.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 取出指定Revision下的文件列表
+ ///
+ public class RevisionObjects : Command {
+ private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
+ private List objects = new List();
+
+ public RevisionObjects(string cwd, string sha) {
+ Cwd = cwd;
+ Args = $"ls-tree -r {sha}";
+ }
+
+ public List 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);
+ }
+ }
+}
diff --git a/src/Commands/SaveChangesToPatch.cs b/src/Commands/SaveChangesToPatch.cs
new file mode 100644
index 00000000..b40b9bee
--- /dev/null
+++ b/src/Commands/SaveChangesToPatch.cs
@@ -0,0 +1,26 @@
+using System.IO;
+
+namespace SourceGit.Commands {
+ ///
+ /// 将Changes保存到文件流中
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs
new file mode 100644
index 00000000..8e02d8c5
--- /dev/null
+++ b/src/Commands/SaveRevisionFile.cs
@@ -0,0 +1,44 @@
+using System.Diagnostics;
+using System.IO;
+
+namespace SourceGit.Commands {
+ ///
+ /// 保存指定版本的文件
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs
new file mode 100644
index 00000000..02d46342
--- /dev/null
+++ b/src/Commands/Stash.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace SourceGit.Commands {
+ ///
+ /// 单个贮藏相关操作
+ ///
+ public class Stash : Command {
+
+ public Stash(string repo) {
+ Cwd = repo;
+ }
+
+ public bool Push(List 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();
+ }
+ }
+}
diff --git a/src/Commands/StashChanges.cs b/src/Commands/StashChanges.cs
new file mode 100644
index 00000000..459a3776
--- /dev/null
+++ b/src/Commands/StashChanges.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 查看Stash中的修改
+ ///
+ public class StashChanges : Command {
+ private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
+ private List changes = new List();
+
+ public StashChanges(string repo, string sha) {
+ Cwd = repo;
+ Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
+ }
+
+ public List 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;
+ }
+ }
+ }
+}
diff --git a/src/Commands/Stashes.cs b/src/Commands/Stashes.cs
new file mode 100644
index 00000000..c40e256c
--- /dev/null
+++ b/src/Commands/Stashes.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 解析当前仓库中的贮藏
+ ///
+ public class Stashes : Command {
+ private static readonly Regex REG_STASH = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$");
+ private List parsed = new List();
+ private Models.Stash current = null;
+
+ public Stashes(string path) {
+ Cwd = path;
+ Args = "stash list --pretty=raw";
+ }
+
+ public List 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);
+ }
+ }
+ }
+}
diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs
new file mode 100644
index 00000000..bf3ba87d
--- /dev/null
+++ b/src/Commands/Submodule.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace SourceGit.Commands {
+ ///
+ /// 子模块
+ ///
+ public class Submodule : Command {
+ private Action onProgress = null;
+
+ public Submodule(string cwd) {
+ Cwd = cwd;
+ }
+
+ public bool Add(string url, string path, bool recursive, Action 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);
+ }
+ }
+}
diff --git a/src/Commands/Submodules.cs b/src/Commands/Submodules.cs
new file mode 100644
index 00000000..4daf69f6
--- /dev/null
+++ b/src/Commands/Submodules.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 获取子模块列表
+ ///
+ public class Submodules : Command {
+ private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$");
+ private List modules = new List();
+
+ public Submodules(string repo) {
+ Cwd = repo;
+ Args = "submodule status";
+ }
+
+ public List 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);
+ }
+ }
+}
diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs
new file mode 100644
index 00000000..88942878
--- /dev/null
+++ b/src/Commands/Tag.cs
@@ -0,0 +1,42 @@
+using System.IO;
+
+namespace SourceGit.Commands {
+
+ ///
+ /// 标签相关指令
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Commands/Tags.cs b/src/Commands/Tags.cs
new file mode 100644
index 00000000..f534c7b2
--- /dev/null
+++ b/src/Commands/Tags.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands {
+ ///
+ /// 解析所有的Tags
+ ///
+ 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 loaded = new List();
+
+ public Tags(string path) {
+ Cwd = path;
+ Args = CMD;
+ }
+
+ public List 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,
+ });
+ }
+ }
+ }
+}
diff --git a/src/Converters/BoolToCollapsed.cs b/src/Converters/BoolToCollapsed.cs
deleted file mode 100644
index 47ce3c73..00000000
--- a/src/Converters/BoolToCollapsed.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows;
-using System.Windows.Data;
-
-namespace SourceGit.Converters {
-
- ///
- /// Same as BoolToVisibilityConverter.
- ///
- public class BoolToCollapsed : IValueConverter {
-
- ///
- /// Implement IValueConverter.Convert
- ///
- ///
- ///
- ///
- ///
- ///
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
- return (bool)value ? Visibility.Visible : Visibility.Collapsed;
- }
-
- ///
- /// Implement IValueConverter.ConvertBack
- ///
- ///
- ///
- ///
- ///
- ///
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/src/Converters/FileStatusToColor.cs b/src/Converters/FileStatusToColor.cs
deleted file mode 100644
index 75af6980..00000000
--- a/src/Converters/FileStatusToColor.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-using System.Windows.Media;
-
-namespace SourceGit.Converters {
-
- ///
- /// Convert file status to brush
- ///
- public class FileStatusToColor : IValueConverter {
-
- ///
- /// Is only test local changes.
- ///
- 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();
- }
- }
-}
diff --git a/src/Converters/FileStatusToIcon.cs b/src/Converters/FileStatusToIcon.cs
deleted file mode 100644
index 85447770..00000000
--- a/src/Converters/FileStatusToIcon.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-
-namespace SourceGit.Converters {
-
- ///
- /// Convert file status to icon.
- ///
- public class FileStatusToIcon : IValueConverter {
-
- ///
- /// Is only test local changes.
- ///
- 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();
- }
- }
-}
diff --git a/src/Converters/FilesDisplayModeToIcon.cs b/src/Converters/FilesDisplayModeToIcon.cs
deleted file mode 100644
index 441c54c7..00000000
--- a/src/Converters/FilesDisplayModeToIcon.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/src/Converters/FilesDisplayModeToVisibility.cs b/src/Converters/FilesDisplayModeToVisibility.cs
deleted file mode 100644
index 2bea761f..00000000
--- a/src/Converters/FilesDisplayModeToVisibility.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/src/Converters/IndentToMargin.cs b/src/Converters/IndentToMargin.cs
deleted file mode 100644
index 5214396b..00000000
--- a/src/Converters/IndentToMargin.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows;
-using System.Windows.Data;
-
-namespace SourceGit.Converters {
-
- ///
- /// Convert indent(horizontal offset) to Margin property
- ///
- public class IndentToMargin : IValueConverter {
-
- ///
- /// Implement IValueConverter.Convert
- ///
- ///
- ///
- ///
- ///
- ///
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
- return new Thickness((double)value, 0, 0, 0);
- }
-
- ///
- /// Implement IValueConverter.ConvertBack
- ///
- ///
- ///
- ///
- ///
- ///
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
- return ((Thickness)value).Left;
- }
- }
-}
diff --git a/src/Converters/IntToRepoColor.cs b/src/Converters/IntToRepoColor.cs
deleted file mode 100644
index 338f5403..00000000
--- a/src/Converters/IntToRepoColor.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows;
-using System.Windows.Data;
-using System.Windows.Media;
-
-namespace SourceGit.Converters {
-
- ///
- /// Integer to color.
- ///
- public class IntToRepoColor : IValueConverter {
-
- ///
- /// All supported colors.
- ///
- public static Brush[] Colors = new Brush[] {
- Brushes.Transparent,
- Brushes.White,
- Brushes.Red,
- Brushes.Orange,
- Brushes.Yellow,
- Brushes.ForestGreen,
- Brushes.Purple,
- Brushes.DeepSkyBlue,
- Brushes.Magenta,
- };
-
- ///
- /// Implement IValueConverter.Convert
- ///
- ///
- ///
- ///
- ///
- ///
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
- return Colors[((int)value) % Colors.Length];
- }
-
- ///
- /// Implement IValueConverter.ConvertBack
- ///
- ///
- ///
- ///
- ///
- ///
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
- return ((Thickness)value).Left;
- }
- }
-}
diff --git a/src/Converters/InverseBoolToCollapsed.cs b/src/Converters/InverseBoolToCollapsed.cs
deleted file mode 100644
index 862bf913..00000000
--- a/src/Converters/InverseBoolToCollapsed.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows;
-using System.Windows.Data;
-
-namespace SourceGit.Converters {
-
- ///
- /// Inverse BoolToCollapsed.
- ///
- public class InverseBoolToCollapsed : IValueConverter {
-
- ///
- /// Implement IValueConverter.Convert
- ///
- ///
- ///
- ///
- ///
- ///
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
- return (bool)value ? Visibility.Collapsed : Visibility.Visible;
- }
-
- ///
- /// Implement IValueConverter.ConvertBack
- ///
- ///
- ///
- ///
- ///
- ///
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/src/Converters/Path.cs b/src/Converters/Path.cs
deleted file mode 100644
index 8062b658..00000000
--- a/src/Converters/Path.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/src/Converters/TreeViewItemDepthToMargin.cs b/src/Converters/TreeViewItemDepthToMargin.cs
deleted file mode 100644
index 8c7eb856..00000000
--- a/src/Converters/TreeViewItemDepthToMargin.cs
+++ /dev/null
@@ -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 {
-
- ///
- /// Convert depth of a TreeViewItem to Margin property.
- ///
- public class TreeViewItemDepthToMargin : IValueConverter {
-
- ///
- /// Indent length
- ///
- public double Indent { get; set; } = 19;
-
- ///
- /// Implement IValueConverter.Convert
- ///
- ///
- ///
- ///
- ///
- ///
- 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);
- }
-
- ///
- /// Implement IValueConvert.ConvertBack
- ///
- ///
- ///
- ///
- ///
- ///
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
- throw new NotImplementedException();
- }
-
- ///
- /// Get parent item.
- ///
- ///
- ///
- 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;
- }
- }
-}
diff --git a/src/Git/Blame.cs b/src/Git/Blame.cs
deleted file mode 100644
index 7985aaa2..00000000
--- a/src/Git/Blame.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System.Collections.Generic;
-
-namespace SourceGit.Git {
-
- ///
- /// Blame
- ///
- public class Blame {
-
- ///
- /// Line content.
- ///
- public class Line {
- public string CommitSHA { get; set; }
- public string Author { get; set; }
- public string Time { get; set; }
- public string Content { get; set; }
- }
-
- ///
- /// Lines
- ///
- public List Lines { get; set; } = new List();
-
- ///
- /// Is binary file?
- ///
- public bool IsBinary { get; set; } = false;
- }
-}
diff --git a/src/Git/Branch.cs b/src/Git/Branch.cs
deleted file mode 100644
index fd1030c0..00000000
--- a/src/Git/Branch.cs
+++ /dev/null
@@ -1,207 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
-
-namespace SourceGit.Git {
-
- ///
- /// Git branch
- ///
- 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+)");
-
- ///
- /// Branch type.
- ///
- public enum Type {
- Normal,
- Feature,
- Release,
- Hotfix,
- }
-
- ///
- /// Branch name
- ///
- public string Name { get; set; } = "";
-
- ///
- /// Full name.
- ///
- public string FullName { get; set; } = "";
-
- ///
- /// Head ref
- ///
- public string Head { get; set; } = "";
-
- ///
- /// Subject for head ref.
- ///
- public string HeadSubject { get; set; } = "";
-
- ///
- /// Is local branch
- ///
- public bool IsLocal { get; set; } = false;
-
- ///
- /// Branch type.
- ///
- public Type Kind { get; set; } = Type.Normal;
-
- ///
- /// Remote name. Only used for remote branch
- ///
- public string Remote { get; set; } = "";
-
- ///
- /// Upstream. Only used for local branches.
- ///
- public string Upstream { get; set; }
-
- ///
- /// Track information for upstream. Only used for local branches.
- ///
- public string UpstreamTrack { get; set; }
-
- ///
- /// Is current branch. Only used for local branches.
- ///
- public bool IsCurrent { get; set; }
-
- ///
- /// Is this branch's HEAD same with upstream?
- ///
- public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
-
- ///
- /// Enable filter in log histories.
- ///
- public bool IsFiltered { get; set; }
-
- ///
- /// Load branches.
- ///
- ///
- public static List Load(Repository repo) {
- var localPrefix = "refs/heads/";
- var remotePrefix = "refs/remotes/";
- var branches = new List();
- var remoteBranches = new List();
-
- 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;
- }
-
- ///
- /// Create new branch.
- ///
- ///
- ///
- ///
- public static void Create(Repository repo, string name, string startPoint) {
- var errs = repo.RunCommand($"branch {name} {startPoint}", null);
- if (errs != null) App.RaiseError(errs);
- }
-
- ///
- /// Rename branch
- ///
- ///
- ///
- public void Rename(Repository repo, string name) {
- var errs = repo.RunCommand($"branch -M {Name} {name}", null);
- if (errs != null) App.RaiseError(errs);
- }
-
- ///
- /// Change upstream
- ///
- ///
- ///
- 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();
- }
-
- ///
- /// Delete branch.
- ///
- ///
- 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();
- }
- }
-}
diff --git a/src/Git/Change.cs b/src/Git/Change.cs
deleted file mode 100644
index c5f9ca4a..00000000
--- a/src/Git/Change.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-using System.Text.RegularExpressions;
-
-namespace SourceGit.Git {
-
- ///
- /// Changed file status.
- ///
- public class Change {
- private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
-
- ///
- /// Status Code
- ///
- public enum Status {
- None,
- Modified,
- Added,
- Deleted,
- Renamed,
- Copied,
- Unmerged,
- Untracked,
- }
-
- ///
- /// Index status
- ///
- public Status Index { get; set; }
-
- ///
- /// Work tree status.
- ///
- public Status WorkTree { get; set; }
-
- ///
- /// Current file path.
- ///
- public string Path { get; set; }
-
- ///
- /// Original file path before this revision.
- ///
- public string OriginalPath { get; set; }
-
- ///
- /// Staged(added) in index?
- ///
- public bool IsAddedToIndex {
- get {
- if (Index == Status.None || Index == Status.Untracked) return false;
- return true;
- }
- }
-
- ///
- /// Is conflict?
- ///
- 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;
- }
- }
-
- ///
- /// Parse change for `--name-status` data.
- ///
- /// Raw data.
- /// Read from commit?
- /// Parsed change instance.
- 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);
- }
- }
- }
- }
- }
-}
diff --git a/src/Git/Commit.cs b/src/Git/Commit.cs
deleted file mode 100644
index bfda00c7..00000000
--- a/src/Git/Commit.cs
+++ /dev/null
@@ -1,364 +0,0 @@
-using System;
-using System.IO;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Text.RegularExpressions;
-
-namespace SourceGit.Git {
-
- ///
- /// Git commit information.
- ///
- 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+.*$");
-
- ///
- /// Object in commit.
- ///
- 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; }
- }
-
- ///
- /// Line of text in file.
- ///
- public class Line {
- public int No { get; set; }
- public string Content { get; set; }
- }
-
- ///
- /// SHA
- ///
- public string SHA { get; set; }
-
- ///
- /// Short SHA.
- ///
- public string ShortSHA => SHA.Substring(0, 8);
-
- ///
- /// Parent commit SHAs.
- ///
- public List Parents { get; set; } = new List();
-
- ///
- /// Author
- ///
- public User Author { get; set; } = new User();
-
- ///
- /// Committer.
- ///
- public User Committer { get; set; } = new User();
-
- ///
- /// Subject
- ///
- public string Subject { get; set; } = "";
-
- ///
- /// Extra message.
- ///
- public string Message { get; set; } = "";
-
- ///
- /// HEAD commit?
- ///
- public bool IsHEAD { get; set; } = false;
-
- ///
- /// Merged in current branch?
- ///
- public bool IsMerged { get; set; } = false;
-
- ///
- /// X offset in graph
- ///
- public double GraphOffset { get; set; } = 0;
-
- ///
- /// Has decorators.
- ///
- public bool HasDecorators => Decorators.Count > 0;
-
- ///
- /// Decorators.
- ///
- public List Decorators { get; set; } = new List();
-
- ///
- /// Read commits.
- ///
- /// Repository
- /// Limitations
- /// Parsed commits.
- public static List Load(Repository repo, string limit) {
- List commits = new List();
- 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;
- }
-
- ///
- /// Get changed file list.
- ///
- ///
- ///
- public List GetChanges(Repository repo) {
- var changes = new List();
- 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;
- }
-
- ///
- /// Get revision files.
- ///
- ///
- ///
- public List