Upgrade project style

This commit is contained in:
ONEO 2020-07-06 10:23:45 +08:00
parent afd22ca85d
commit baa6c1445a
136 changed files with 29 additions and 770 deletions

35
SourceGit/Git/Blame.cs Normal file
View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace SourceGit.Git {
/// <summary>
/// Blame
/// </summary>
public class Blame {
/// <summary>
/// Block content.
/// </summary>
public class Block {
public string CommitSHA { get; set; }
public string Author { get; set; }
public string Time { get; set; }
public string Content { get; set; }
}
/// <summary>
/// Blocks
/// </summary>
public List<Block> Blocks { get; set; } = new List<Block>();
/// <summary>
/// Is binary file?
/// </summary>
public bool IsBinary { get; set; } = false;
/// <summary>
/// Line count.
/// </summary>
public int LineCount { get; set; } = 0;
}
}

190
SourceGit/Git/Branch.cs Normal file
View file

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git branch
/// </summary>
public class Branch {
private static readonly string PRETTY_FORMAT = @"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)";
private static readonly Regex PARSE = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
private static readonly Regex AHEAD = new Regex(@"ahead (\d+)");
private static readonly Regex BEHIND = new Regex(@"behind (\d+)");
/// <summary>
/// Branch type.
/// </summary>
public enum Type {
Normal,
Feature,
Release,
Hotfix,
}
/// <summary>
/// Branch name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Full name.
/// </summary>
public string FullName { get; set; }
/// <summary>
/// Head ref
/// </summary>
public string Head { get; set; }
/// <summary>
/// Subject for head ref.
/// </summary>
public string HeadSubject { get; set; }
/// <summary>
/// Is local branch
/// </summary>
public bool IsLocal { get; set; }
/// <summary>
/// Branch type.
/// </summary>
public Type Kind { get; set; } = Type.Normal;
/// <summary>
/// Remote name. Only used for remote branch
/// </summary>
public string Remote { get; set; }
/// <summary>
/// Upstream. Only used for local branches.
/// </summary>
public string Upstream { get; set; }
/// <summary>
/// Track information for upstream. Only used for local branches.
/// </summary>
public string UpstreamTrack { get; set; }
/// <summary>
/// Is current branch. Only used for local branches.
/// </summary>
public bool IsCurrent { get; set; }
/// <summary>
/// Is this branch's HEAD same with upstream?
/// </summary>
public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load branches.
/// </summary>
/// <param name="repo"></param>
public static List<Branch> Load(Repository repo) {
var localPrefix = "refs/heads/";
var remotePrefix = "refs/remotes/";
var branches = new List<Branch>();
var remoteBranches = new List<string>();
repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => {
var match = PARSE.Match(line);
if (!match.Success) return;
var branch = new Branch();
var refname = match.Groups[1].Value;
if (refname.EndsWith("/HEAD")) return;
if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) {
branch.Name = refname.Substring(localPrefix.Length);
branch.IsLocal = true;
} else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) {
var name = refname.Substring(remotePrefix.Length);
branch.Remote = name.Substring(0, name.IndexOf('/'));
branch.Name = name;
branch.IsLocal = false;
remoteBranches.Add(refname);
}
branch.FullName = refname;
branch.Head = match.Groups[2].Value;
branch.IsCurrent = match.Groups[3].Value == "*";
branch.Upstream = match.Groups[4].Value;
branch.UpstreamTrack = ParseTrack(match.Groups[5].Value);
branch.HeadSubject = match.Groups[6].Value;
branches.Add(branch);
});
// Fixed deleted remote branch
foreach (var b in branches) {
if (!string.IsNullOrEmpty(b.Upstream) && !remoteBranches.Contains(b.Upstream)) {
b.Upstream = null;
}
}
return branches;
}
/// <summary>
/// Create new branch.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
public static void Create(Repository repo, string name, string startPoint) {
var errs = repo.RunCommand($"branch {name} {startPoint}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Rename branch
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
public void Rename(Repository repo, string name) {
var errs = repo.RunCommand($"branch -M {Name} {name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Delete branch.
/// </summary>
/// <param name="repo"></param>
public void Delete(Repository repo) {
string errs = null;
if (!IsLocal) {
errs = repo.RunCommand($"-c credential.helper=manager push {Remote} --delete {Name.Substring(Name.IndexOf('/')+1)}", null);
} else {
errs = repo.RunCommand($"branch -D {Name}", null);
}
if (errs != null) App.RaiseError(errs);
}
private static string ParseTrack(string data) {
if (string.IsNullOrEmpty(data)) return "";
string track = "";
var ahead = AHEAD.Match(data);
if (ahead.Success) {
track += ahead.Groups[1].Value + "↑ ";
}
var behind = BEHIND.Match(data);
if (behind.Success) {
track += behind.Groups[1].Value + "↓";
}
return track.Trim();
}
}
}

147
SourceGit/Git/Change.cs Normal file
View file

@ -0,0 +1,147 @@
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Changed file status.
/// </summary>
public class Change {
private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
/// <summary>
/// Status Code
/// </summary>
public enum Status {
None,
Modified,
Added,
Deleted,
Renamed,
Copied,
Unmerged,
Untracked,
}
/// <summary>
/// Index status
/// </summary>
public Status Index { get; set; }
/// <summary>
/// Work tree status.
/// </summary>
public Status WorkTree { get; set; }
/// <summary>
/// Current file path.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Original file path before this revision.
/// </summary>
public string OriginalPath { get; set; }
/// <summary>
/// Staged(added) in index?
/// </summary>
public bool IsAddedToIndex {
get {
if (Index == Status.None || Index == Status.Untracked) return false;
return true;
}
}
/// <summary>
/// Is conflict?
/// </summary>
public bool IsConflit {
get {
if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true;
if (Index == Status.Added && WorkTree == Status.Added) return true;
if (Index == Status.Deleted && WorkTree == Status.Deleted) return true;
return false;
}
}
/// <summary>
/// Parse change for `--name-status` data.
/// </summary>
/// <param name="data">Raw data.</param>
/// <param name="fromCommit">Read from commit?</param>
/// <returns>Parsed change instance.</returns>
public static Change Parse(string data, bool fromCommit = false) {
var match = FORMAT.Match(data);
if (!match.Success) return null;
var change = new Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
if (fromCommit) {
switch (status[0]) {
case 'M': change.Set(Status.Modified); break;
case 'A': change.Set(Status.Added); break;
case 'D': change.Set(Status.Deleted); break;
case 'R': change.Set(Status.Renamed); break;
case 'C': change.Set(Status.Copied); break;
default: return null;
}
} else {
switch (status) {
case " M": change.Set(Status.None, Status.Modified); break;
case " A": change.Set(Status.None, Status.Added); break;
case " D": change.Set(Status.None, Status.Deleted); break;
case " R": change.Set(Status.None, Status.Renamed); break;
case " C": change.Set(Status.None, Status.Copied); break;
case "M": change.Set(Status.Modified, Status.None); break;
case "MM": change.Set(Status.Modified, Status.Modified); break;
case "MD": change.Set(Status.Modified, Status.Deleted); break;
case "A": change.Set(Status.Added, Status.None); break;
case "AM": change.Set(Status.Added, Status.Modified); break;
case "AD": change.Set(Status.Added, Status.Deleted); break;
case "D": change.Set(Status.Deleted, Status.None); break;
case "R": change.Set(Status.Renamed, Status.None); break;
case "RM": change.Set(Status.Renamed, Status.Modified); break;
case "RD": change.Set(Status.Renamed, Status.Deleted); break;
case "C": change.Set(Status.Copied, Status.None); break;
case "CM": change.Set(Status.Copied, Status.Modified); break;
case "CD": change.Set(Status.Copied, Status.Deleted); break;
case "DR": change.Set(Status.Deleted, Status.Renamed); break;
case "DC": change.Set(Status.Deleted, Status.Copied); break;
case "DD": change.Set(Status.Deleted, Status.Deleted); break;
case "AU": change.Set(Status.Added, Status.Unmerged); break;
case "UD": change.Set(Status.Unmerged, Status.Deleted); break;
case "UA": change.Set(Status.Unmerged, Status.Added); break;
case "DU": change.Set(Status.Deleted, Status.Unmerged); break;
case "AA": change.Set(Status.Added, Status.Added); break;
case "UU": change.Set(Status.Unmerged, Status.Unmerged); break;
case "??": change.Set(Status.Untracked, Status.Untracked); break;
default: return null;
}
}
if (change.Path[0] == '"') change.Path = change.Path.Substring(1, change.Path.Length - 2);
if (!string.IsNullOrEmpty(change.OriginalPath) && change.OriginalPath[0] == '"') change.OriginalPath = change.OriginalPath.Substring(1, change.OriginalPath.Length - 2);
return change;
}
private void Set(Status index, Status workTree = Status.None) {
Index = index;
WorkTree = workTree;
if (index == Status.Renamed || workTree == Status.Renamed) {
var idx = Path.IndexOf('\t');
if (idx >= 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 1);
} else {
idx = Path.IndexOf(" -> ");
if (idx > 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 4);
}
}
}
}
}
}

262
SourceGit/Git/Commit.cs Normal file
View file

@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git commit information.
/// </summary>
public class Commit {
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Short SHA.
/// </summary>
public string ShortSHA => SHA.Substring(0, 8);
/// <summary>
/// Parent commit SHAs.
/// </summary>
public List<string> Parents { get; set; } = new List<string>();
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Committer.
/// </summary>
public User Committer { get; set; } = new User();
/// <summary>
/// Subject
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// Extra message.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// HEAD commit?
/// </summary>
public bool IsHEAD { get; set; } = false;
/// <summary>
/// Merged in current branch?
/// </summary>
public bool IsMerged { get; set; } = false;
/// <summary>
/// X offset in graph
/// </summary>
public double GraphOffset { get; set; } = 0;
/// <summary>
/// Has decorators.
/// </summary>
public bool HasDecorators => Decorators.Count > 0;
/// <summary>
/// Decorators.
/// </summary>
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
/// <summary>
/// Read commits.
/// </summary>
/// <param name="repo">Repository</param>
/// <param name="limit">Limitations</param>
/// <returns>Parsed commits.</returns>
public static List<Commit> Load(Repository repo, string limit) {
List<Commit> commits = new List<Commit>();
Commit current = null;
bool bSkippingGpgsig = false;
bool findHead = false;
repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => {
if (bSkippingGpgsig) {
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false;
return;
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) {
bSkippingGpgsig = true;
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
current = new Commit();
ParseSHA(current, line.Substring("commit ".Length));
if (!findHead) findHead = current.IsHEAD;
return;
}
if (current == null) return;
if (line.StartsWith("tree ", StringComparison.Ordinal)) {
return;
} else if (line.StartsWith("parent ", StringComparison.Ordinal)) {
current.Parents.Add(line.Substring("parent ".Length));
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
current.Author.Parse(line);
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) {
current.Committer.Parse(line);
} else if (string.IsNullOrEmpty(current.Subject)) {
current.Subject = line.Trim();
} else {
current.Message += (line.Trim() + "\n");
}
});
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
if (!findHead && commits.Count > 0) {
var startInfo = new ProcessStartInfo();
startInfo.FileName = Preference.Instance.GitExecutable;
startInfo.Arguments = $"merge-base --is-ancestor {commits[0].SHA} HEAD";
startInfo.WorkingDirectory = repo.Path;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = false;
startInfo.RedirectStandardError = false;
var proc = new Process() { StartInfo = startInfo };
proc.Start();
proc.WaitForExit();
commits[0].IsMerged = proc.ExitCode == 0;
proc.Close();
}
return commits;
}
/// <summary>
/// Get changed file list.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
var changes = new List<Change>();
var regex = new Regex(@"^[MADRC]\d*\s*.*$");
var errs = repo.RunCommand($"show --name-status {SHA}", line => {
if (!regex.IsMatch(line)) return;
var change = Change.Parse(line, true);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Get revision files.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<string> GetFiles(Repository repo) {
var files = new List<string>();
var errs = repo.RunCommand($"ls-tree --name-only -r {SHA}", line => {
files.Add(line);
});
if (errs != null) App.RaiseError(errs);
return files;
}
/// <summary>
/// Get file content.
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <returns></returns>
public string GetTextFileContent(Repository repo, string file) {
var data = new List<string>();
var isBinary = false;
var count = 0;
var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => {
if (isBinary) return;
count++;
if (data.Count >= 1000) return;
if (line.IndexOf('\0') >= 0) {
isBinary = true;
data.Clear();
data.Add("BINARY FILE PREVIEW NOT SUPPORTED!");
return;
}
data.Add(line);
});
if (!isBinary && count > 1000) {
data.Add("...");
data.Add($"Total {count} lines. Hide {count-1000} lines.");
}
if (errs != null) App.RaiseError(errs);
return string.Join("\n", data);
}
private static void ParseSHA(Commit commit, string data) {
var decoratorStart = data.IndexOf('(');
if (decoratorStart < 0) {
commit.SHA = data.Trim();
return;
}
commit.SHA = data.Substring(0, decoratorStart).Trim();
var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs) {
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.Tag,
Name = d.Substring(15).Trim()
});
} else if (d.EndsWith("/HEAD")) {
continue;
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) {
commit.IsHEAD = true;
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim()
});
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim()
});
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim()
});
}
}
}
}
}

View file

@ -0,0 +1,21 @@
namespace SourceGit.Git {
/// <summary>
/// Decorator type.
/// </summary>
public enum DecoratorType {
None,
CurrentBranchHead,
LocalBranchHead,
RemoteBranchHead,
Tag,
}
/// <summary>
/// Commit decorator.
/// </summary>
public class Decorator {
public DecoratorType Type { get; set; }
public string Name { get; set; }
}
}

202
SourceGit/Git/MergeTool.cs Normal file
View file

@ -0,0 +1,202 @@
using Microsoft.Win32;
using SourceGit.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// External merge tool
/// </summary>
public class MergeTool {
/// <summary>
/// Display name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Executable file name.
/// </summary>
public string ExecutableName { get; set; }
/// <summary>
/// Command line parameter.
/// </summary>
public string Parameter { get; set; }
/// <summary>
/// Auto finder.
/// </summary>
public Func<string> Finder { get; set; }
/// <summary>
/// Is this merge tool configured.
/// </summary>
public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName);
/// <summary>
/// Supported merge tools.
/// </summary>
public static List<MergeTool> Supported = new List<MergeTool>() {
new MergeTool("--", "", "", FindInvalid),
new MergeTool("Araxis Merge", "Compare.exe", "/wait /merge /3 /a1 \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindAraxisMerge),
new MergeTool("Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare),
new MergeTool("KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3),
new MergeTool("P4Merge", "p4merge.exe", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindP4Merge),
new MergeTool("Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge),
new MergeTool("Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge),
new MergeTool("Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode),
};
/// <summary>
/// Finder for invalid merge tool.
/// </summary>
/// <returns></returns>
public static string FindInvalid() {
return "--";
}
/// <summary>
/// Find araxis merge tool install path.
/// </summary>
/// <returns></returns>
public static string FindAraxisMerge() {
var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find kdiff3.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindKDiff3() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext");
if (kdiff == null) return "";
return kdiff.GetValue("diffcommand") as string;
}
/// <summary>
/// Finder for p4merge
/// </summary>
/// <returns></returns>
public static string FindP4Merge() {
var path = @"C:\Program Files\Perforce\p4merge.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find BComp.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindBCompare() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare");
if (bc == null) return "";
var exec = bc.GetValue("ExePath") as string;
var dir = Path.GetDirectoryName(exec);
return $"{dir}\\BComp.exe";
}
/// <summary>
/// Find TortoiseMerge.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindTortoiseMerge() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN");
if (tortoiseSVN == null) return "";
return tortoiseSVN.GetValue("TMergePath") as string;
}
/// <summary>
/// Find vsDiffMerge.exe.
/// </summary>
/// <returns></returns>
public static string FindVSMerge() {
var dir = @"C:\Program Files (x86)\Microsoft Visual Studio";
if (Directory.Exists($"{dir}\\2019")) {
dir += "\\2019";
} else if (Directory.Exists($"{dir}\\2017")) {
dir += "\\2017";
} else {
return "";
}
if (Directory.Exists($"{dir}\\Community")) {
dir += "\\Community";
} else if (Directory.Exists($"{dir}\\Enterprise")) {
dir += "\\Enterprise";
} else if (Directory.Exists($"{dir}\\Professional")) {
dir += "\\Professional";
} else {
return "";
}
return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe";
}
/// <summary>
/// Find VSCode executable file path.
/// </summary>
/// <returns></returns>
public static string FindVSCode() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
return "";
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name"></param>
/// <param name="exe"></param>
/// <param name="param"></param>
/// <param name="finder"></param>
public MergeTool(string name, string exe, string param, Func<string> finder) {
Name = name;
ExecutableName = exe;
Parameter = param;
Finder = finder;
}
}
}

296
SourceGit/Git/Preference.cs Normal file
View file

@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace SourceGit.Git {
/// <summary>
/// User's preference settings. Serialized to
/// </summary>
public class Preference {
/// <summary>
/// Group(Virtual folder) for watched repositories.
/// </summary>
public class Group {
/// <summary>
/// Unique ID of this group.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Parent ID.
/// </summary>
public string ParentId { get; set; }
/// <summary>
/// Cache UI IsExpended status.
/// </summary>
public bool IsExpended { get; set; }
}
#region STATICS
/// <summary>
/// Storage path for Preference.
/// </summary>
private static readonly string SAVE_PATH = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"preference.xml");
/// <summary>
/// Runtime singleton instance.
/// </summary>
private static Preference instance = null;
public static Preference Instance {
get {
if (instance == null) Load();
return instance;
}
set {
instance = value;
}
}
#endregion
#region SETTING_GIT
/// <summary>
/// Git executable file path.
/// </summary>
public string GitExecutable { get; set; }
/// <summary>
/// Default clone directory.
/// </summary>
public string GitDefaultCloneDir { get; set; }
#endregion
#region SETTING_MERGE_TOOL
/// <summary>
/// Selected merge tool.
/// </summary>
public int MergeTool { get; set; } = 0;
/// <summary>
/// Executable file path for merge tool.
/// </summary>
public string MergeExecutable { get; set; } = "--";
#endregion
#region SETTING_UI
/// <summary>
/// Main window's width
/// </summary>
public double UIMainWindowWidth { get; set; }
/// <summary>
/// Main window's height
/// </summary>
public double UIMainWindowHeight { get; set; }
/// <summary>
/// Use light color theme.
/// </summary>
public bool UIUseLightTheme { get; set; }
/// <summary>
/// Show/Hide tags' list view.
/// </summary>
public bool UIShowTags { get; set; } = true;
/// <summary>
/// Use horizontal layout for histories.
/// </summary>
public bool UIUseHorizontalLayout { get; set; }
/// <summary>
/// Use list instead of tree in unstaged view
/// </summary>
public bool UIUseListInUnstaged { get; set; }
/// <summary>
/// Use list instead of tree in staged view.
/// </summary>
public bool UIUseListInStaged { get; set; }
/// <summary>
/// Use list instead of tree in change view.
/// </summary>
public bool UIUseListInChanges { get; set; }
#endregion
#region SETTING_REPOS
/// <summary>
/// Groups for repositories.
/// </summary>
public List<Group> Groups { get; set; } = new List<Group>();
/// <summary>
/// Watched repositories.
/// </summary>
public List<Repository> Repositories { get; set; } = new List<Git.Repository>();
#endregion
#region METHODS_LOAD_SAVE
/// <summary>
/// Load preference from disk.
/// </summary>
/// <returns>Loaded preference instance.</returns>
public static void Load() {
if (!File.Exists(SAVE_PATH)) {
instance = new Preference();
return;
}
var stream = new FileStream(SAVE_PATH, FileMode.Open);
var reader = new XmlSerializer(typeof(Preference));
instance = (Preference)reader.Deserialize(stream);
stream.Close();
}
/// <summary>
/// Save current preference into disk.
/// </summary>
public static void Save() {
if (instance == null) return;
var dir = Path.GetDirectoryName(SAVE_PATH);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var stream = new FileStream(SAVE_PATH, FileMode.Create);
var writer = new XmlSerializer(typeof(Preference));
writer.Serialize(stream, instance);
stream.Flush();
stream.Close();
}
#endregion
#region METHODS_ON_GROUP
/// <summary>
/// Add new group(virtual folder).
/// </summary>
/// <param name="name">Display name.</param>
/// <param name="parentId">Parent group ID.</param>
/// <returns>Added group instance.</returns>
public Group AddGroup(string name, string parentId) {
var group = new Group() {
Name = name,
Id = Guid.NewGuid().ToString(),
ParentId = parentId,
IsExpended = false,
};
Groups.Add(group);
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
return group;
}
/// <summary>
/// Find group by ID.
/// </summary>
/// <param name="id">Unique ID</param>
/// <returns>Founded group's instance.</returns>
public Group FindGroup(string id) {
foreach (var group in Groups) {
if (group.Id == id) return group;
}
return null;
}
/// <summary>
/// Rename group.
/// </summary>
/// <param name="id">Unique ID</param>
/// <param name="newName">New name.</param>
public void RenameGroup(string id, string newName) {
foreach (var group in Groups) {
if (group.Id == id) {
group.Name = newName;
break;
}
}
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a group.
/// </summary>
/// <param name="id">Unique ID</param>
public void RemoveGroup(string id) {
int removedIdx = -1;
for (int i = 0; i < Groups.Count; i++) {
if (Groups[i].Id == id) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Groups.RemoveAt(removedIdx);
}
#endregion
#region METHODS_ON_REPOS
/// <summary>
/// Add repository.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="groupId">Group's ID</param>
/// <returns>Added repository instance.</returns>
public Repository AddRepository(string path, string groupId) {
var repo = FindRepository(path);
if (repo != null) return repo;
var dir = new DirectoryInfo(path);
repo = new Repository() {
Path = dir.FullName,
Name = dir.Name,
GroupId = groupId,
LastOpenTime = 0,
};
Repositories.Add(repo);
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
return repo;
}
/// <summary>
/// Find repository by path.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <returns>Founded repository instance.</returns>
public Repository FindRepository(string path) {
var dir = new DirectoryInfo(path);
foreach (var repo in Repositories) {
if (repo.Path == dir.FullName) return repo;
}
return null;
}
/// <summary>
/// Change a repository's display name in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="newName">New name</param>
public void RenameRepository(string path, string newName) {
var repo = FindRepository(path);
if (repo == null) return;
repo.Name = newName;
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a repository in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
public void RemoveRepository(string path) {
var dir = new DirectoryInfo(path);
var removedIdx = -1;
for (int i = 0; i < Repositories.Count; i++) {
if (Repositories[i].Path == dir.FullName) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Repositories.RemoveAt(removedIdx);
}
#endregion
}
}

97
SourceGit/Git/Remote.cs Normal file
View file

@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git remote
/// </summary>
public class Remote {
private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
/// <summary>
/// Name of this remote
/// </summary>
public string Name { get; set; }
/// <summary>
/// URL
/// </summary>
public string URL { get; set; }
/// <summary>
/// Parsing remote
/// </summary>
/// <param name="repo">Repository</param>
/// <returns></returns>
public static List<Remote> Load(Repository repo) {
var remotes = new List<Remote>();
var added = new List<string>();
repo.RunCommand("remote -v", data => {
var match = FORMAT.Match(data);
if (!match.Success) return;
var remote = new Remote() {
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (added.Contains(remote.Name)) return;
added.Add(remote.Name);
remotes.Add(remote);
});
return remotes;
}
/// <summary>
/// Add new remote
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
public static void Add(Repository repo, string name, string url) {
var errs = repo.RunCommand($"remote add {name} {url}", null);
if (errs != null) {
App.RaiseError(errs);
} else {
repo.Fetch(null, true, null);
}
}
/// <summary>
/// Delete remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="remote"></param>
public static void Delete(Repository repo, string remote) {
var errs = repo.RunCommand($"remote remove {remote}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Edit remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
public void Edit(Repository repo, string name, string url) {
string errs = null;
if (name != Name) {
errs = repo.RunCommand($"remote rename {Name} {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
}
if (url != URL) {
errs = repo.RunCommand($"remote set-url {name} {url}", null);
if (errs != null) App.RaiseError(errs);
}
}
}
}

1058
SourceGit/Git/Repository.cs Normal file

File diff suppressed because it is too large Load diff

101
SourceGit/Git/Stash.cs Normal file
View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// Git stash
/// </summary>
public class Stash {
/// <summary>
/// SHA for this stash
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Stash push.
/// </summary>
/// <param name="repo"></param>
/// <param name="includeUntracked"></param>
/// <param name="message"></param>
/// <param name="files"></param>
public static void Push(Repository repo, bool includeUntracked, string message, List<string> files) {
string specialFiles = "";
if (files.Count > 0) {
specialFiles = " --";
foreach (var f in files) specialFiles += $" \"{f}\"";
}
string args = "stash push ";
if (includeUntracked) args += "-u ";
if (!string.IsNullOrEmpty(message)) args += $"-m \"{message}\" ";
var errs = repo.RunCommand(args + specialFiles, null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Get changed file list in this stash.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
List<Change> changes = new List<Change>();
var errs = repo.RunCommand($"diff --name-status --pretty=format: {SHA}^ {SHA}", line => {
var change = Change.Parse(line);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Apply stash.
/// </summary>
/// <param name="repo"></param>
public void Apply(Repository repo) {
var errs = repo.RunCommand($"stash apply -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Pop stash
/// </summary>
/// <param name="repo"></param>
public void Pop(Repository repo) {
var errs = repo.RunCommand($"stash pop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Drop stash
/// </summary>
/// <param name="repo"></param>
public void Drop(Repository repo) {
var errs = repo.RunCommand($"stash drop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

118
SourceGit/Git/Tag.cs Normal file
View file

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git tag.
/// </summary>
public class Tag {
private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)");
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load all tags
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public static List<Tag> Load(Repository repo) {
var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
var tags = new List<Tag>();
repo.RunCommand(args, line => {
var match = FORMAT.Match(line);
if (!match.Success) return;
var name = match.Groups[1].Value;
var commit = match.Groups[2].Value;
var dereference = match.Groups[3].Value;
if (string.IsNullOrEmpty(dereference)) {
tags.Add(new Tag() {
Name = name,
SHA = commit,
});
} else {
tags.Add(new Tag() {
Name = name,
SHA = dereference,
});
}
});
return tags;
}
/// <summary>
/// Add new tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
/// <param name="message"></param>
public static void Add(Repository repo, string name, string startPoint, string message) {
var args = $"tag -a {name} {startPoint} ";
if (!string.IsNullOrEmpty(message)) {
string temp = Path.GetTempFileName();
File.WriteAllText(temp, message);
args += $"-F \"{temp}\"";
} else {
args += $"-m {name}";
}
var errs = repo.RunCommand(args, null);
if (errs != null) App.RaiseError(errs);
else repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Delete tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="push"></param>
public static void Delete(Repository repo, string name, bool push) {
var errs = repo.RunCommand($"tag --delete {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
if (push) {
var remotes = repo.Remotes();
foreach (var r in remotes) {
repo.RunCommand($"-c credential.helper=manager push --delete {r.Name} refs/tags/{name}", null);
}
}
repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Push tag to remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="remote"></param>
public static void Push(Repository repo, string name, string remote) {
var errs = repo.RunCommand($"-c credential.helper=manager push {remote} refs/tags/{name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

42
SourceGit/Git/User.cs Normal file
View file

@ -0,0 +1,42 @@
using System;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git user.
/// </summary>
public class User {
private static readonly Regex FORMAT = new Regex(@"\w+ (.*) <([\w\.\-_]+@[\w\.\-_]+)> (\d{10}) [\+\-]\d+");
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Email.
/// </summary>
public string Email { get; set; } = "";
/// <summary>
/// Operation time.
/// </summary>
public string Time { get; set; } = "";
/// <summary>
/// Parse user from raw string.
/// </summary>
/// <param name="data">Raw string</param>
public void Parse(string data) {
var match = FORMAT.Match(data);
if (!match.Success) return;
var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(int.Parse(match.Groups[3].Value));
Name = match.Groups[1].Value;
Email = match.Groups[2].Value;
Time = time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
}
}