mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-30 08:34:59 +00:00
refactor(*): re-arrange project
This commit is contained in:
parent
7558bbd4f2
commit
5ce9cffcee
140 changed files with 4980 additions and 5003 deletions
35
src/Git/Blame.cs
Normal file
35
src/Git/Blame.cs
Normal 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
src/Git/Branch.cs
Normal file
190
src/Git/Branch.cs
Normal 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; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Branch type.
|
||||
/// </summary>
|
||||
public Type Kind { get; set; } = Type.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// Remote name. Only used for remote branch
|
||||
/// </summary>
|
||||
public string Remote { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Upstream. Only used for local branches.
|
||||
/// </summary>
|
||||
public string Upstream { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Track information for upstream. Only used for local branches.
|
||||
/// </summary>
|
||||
public string UpstreamTrack { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is current branch. Only used for local branches.
|
||||
/// </summary>
|
||||
public bool IsCurrent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this branch's HEAD same with upstream?
|
||||
/// </summary>
|
||||
public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
|
||||
|
||||
/// <summary>
|
||||
/// Enable filter in log histories.
|
||||
/// </summary>
|
||||
public bool IsFiltered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Load branches.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public static List<Branch> Load(Repository repo) {
|
||||
var localPrefix = "refs/heads/";
|
||||
var remotePrefix = "refs/remotes/";
|
||||
var branches = new List<Branch>();
|
||||
var remoteBranches = new List<string>();
|
||||
|
||||
repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => {
|
||||
var match = PARSE.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var branch = new Branch();
|
||||
var refname = match.Groups[1].Value;
|
||||
if (refname.EndsWith("/HEAD")) return;
|
||||
|
||||
if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) {
|
||||
branch.Name = refname.Substring(localPrefix.Length);
|
||||
branch.IsLocal = true;
|
||||
} else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) {
|
||||
var name = refname.Substring(remotePrefix.Length);
|
||||
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
src/Git/Change.cs
Normal file
147
src/Git/Change.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
296
src/Git/Commit.cs
Normal file
296
src/Git/Commit.cs
Normal file
|
@ -0,0 +1,296 @@
|
|||
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>
|
||||
/// Object in commit.
|
||||
/// </summary>
|
||||
public class Object {
|
||||
public enum Type {
|
||||
Tag,
|
||||
Blob,
|
||||
Tree,
|
||||
Commit,
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public Type Kind { get; set; }
|
||||
public string SHA { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<Object> GetFiles(Repository repo) {
|
||||
var files = new List<Object>();
|
||||
var test = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
|
||||
|
||||
var errs = repo.RunCommand($"ls-tree -r {SHA}", line => {
|
||||
var match = test.Match(line);
|
||||
if (!match.Success) return;
|
||||
|
||||
var obj = new Object();
|
||||
obj.Path = match.Groups[3].Value;
|
||||
obj.Kind = Object.Type.Blob;
|
||||
obj.SHA = match.Groups[2].Value;
|
||||
|
||||
switch (match.Groups[1].Value) {
|
||||
case "tag": obj.Kind = Object.Type.Tag; break;
|
||||
case "blob": obj.Kind = Object.Type.Blob; break;
|
||||
case "tree": obj.Kind = Object.Type.Tree; break;
|
||||
case "commit": obj.Kind = Object.Type.Commit; break;
|
||||
}
|
||||
|
||||
files.Add(obj);
|
||||
});
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
return files;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get file content.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
public string GetTextFileContent(Repository repo, string file, out bool isBinary) {
|
||||
var data = new List<string>();
|
||||
var count = 0;
|
||||
var binary = false;
|
||||
|
||||
var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => {
|
||||
if (binary) return;
|
||||
|
||||
count++;
|
||||
if (data.Count >= 1000) return;
|
||||
|
||||
if (line.IndexOf('\0') >= 0) {
|
||||
binary = true;
|
||||
data.Clear();
|
||||
data.Add("BINARY FILE PREVIEW NOT SUPPORTED!");
|
||||
return;
|
||||
}
|
||||
|
||||
data.Add(line);
|
||||
});
|
||||
|
||||
if (!binary && count > 1000) {
|
||||
data.Add("...");
|
||||
data.Add($"Total {count} lines. Hide {count-1000} lines.");
|
||||
}
|
||||
|
||||
isBinary = binary;
|
||||
|
||||
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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
src/Git/Decorator.cs
Normal file
21
src/Git/Decorator.cs
Normal 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; }
|
||||
}
|
||||
}
|
263
src/Git/Diff.cs
Normal file
263
src/Git/Diff.cs
Normal file
|
@ -0,0 +1,263 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Diff helper.
|
||||
/// </summary>
|
||||
public class Diff {
|
||||
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
|
||||
|
||||
/// <summary>
|
||||
/// Line mode.
|
||||
/// </summary>
|
||||
public enum LineMode {
|
||||
Normal,
|
||||
Indicator,
|
||||
Empty,
|
||||
Added,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Side
|
||||
/// </summary>
|
||||
public enum Side {
|
||||
Left,
|
||||
Right,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary change.
|
||||
/// </summary>
|
||||
public class BinaryChange {
|
||||
public long Size = 0;
|
||||
public long PreSize = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Block
|
||||
/// </summary>
|
||||
public class Block {
|
||||
public Side Side = Side.Both;
|
||||
public LineMode Mode = LineMode.Normal;
|
||||
public int LeftStart = 0;
|
||||
public int RightStart = 0;
|
||||
public int Count = 0;
|
||||
public StringBuilder Builder = new StringBuilder();
|
||||
|
||||
public bool IsLeftDelete => Side == Side.Left && Mode == LineMode.Deleted;
|
||||
public bool IsRightAdded => Side == Side.Right && Mode == LineMode.Added;
|
||||
public bool IsBothSideNormal => Side == Side.Both && Mode == LineMode.Normal;
|
||||
public bool CanShowNumber => Mode != LineMode.Indicator && Mode != LineMode.Empty;
|
||||
|
||||
public void Append(string data) {
|
||||
if (Count > 0) Builder.AppendLine();
|
||||
Builder.Append(data);
|
||||
Count++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diff result.
|
||||
/// </summary>
|
||||
public class Result {
|
||||
public bool IsValid = false;
|
||||
public bool IsBinary = false;
|
||||
public List<Block> Blocks = new List<Block>();
|
||||
public int LeftLineCount = 0;
|
||||
public int RightLineCount = 0;
|
||||
|
||||
public void SetBinary() {
|
||||
IsValid = true;
|
||||
IsBinary = true;
|
||||
}
|
||||
|
||||
public void Add(Block b) {
|
||||
if (b.Count == 0) return;
|
||||
|
||||
switch (b.Side) {
|
||||
case Side.Left:
|
||||
LeftLineCount += b.Count;
|
||||
break;
|
||||
case Side.Right:
|
||||
RightLineCount += b.Count;
|
||||
break;
|
||||
default:
|
||||
LeftLineCount += b.Count;
|
||||
RightLineCount += b.Count;
|
||||
break;
|
||||
}
|
||||
|
||||
Blocks.Add(b);
|
||||
}
|
||||
|
||||
public void Fit() {
|
||||
if (LeftLineCount > RightLineCount) {
|
||||
var b = new Block();
|
||||
b.Side = Side.Right;
|
||||
b.Mode = LineMode.Empty;
|
||||
|
||||
var delta = LeftLineCount - RightLineCount;
|
||||
for (int i = 0; i < delta; i++) b.Append("");
|
||||
|
||||
Add(b);
|
||||
} else if (LeftLineCount < RightLineCount) {
|
||||
var b = new Block();
|
||||
b.Side = Side.Left;
|
||||
b.Mode = LineMode.Empty;
|
||||
|
||||
var delta = RightLineCount - LeftLineCount;
|
||||
for (int i = 0; i < delta; i++) b.Append("");
|
||||
|
||||
Add(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run diff process.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static Result Run(Repository repo, string args) {
|
||||
var rs = new Result();
|
||||
var current = new Block();
|
||||
var left = 0;
|
||||
var right = 0;
|
||||
|
||||
repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => {
|
||||
if (rs.IsBinary) return;
|
||||
|
||||
if (!rs.IsValid) {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (!match.Success) {
|
||||
if (line.StartsWith("Binary ")) rs.SetBinary();
|
||||
return;
|
||||
}
|
||||
|
||||
rs.IsValid = true;
|
||||
left = int.Parse(match.Groups[1].Value);
|
||||
right = int.Parse(match.Groups[2].Value);
|
||||
current.Mode = LineMode.Indicator;
|
||||
current.Append(line);
|
||||
} else {
|
||||
if (line[0] == '-') {
|
||||
if (current.IsLeftDelete) {
|
||||
current.Append(line.Substring(1));
|
||||
} else {
|
||||
rs.Add(current);
|
||||
|
||||
current = new Block();
|
||||
current.Side = Side.Left;
|
||||
current.Mode = LineMode.Deleted;
|
||||
current.LeftStart = left;
|
||||
current.Append(line.Substring(1));
|
||||
}
|
||||
|
||||
left++;
|
||||
} else if (line[0] == '+') {
|
||||
if (current.IsRightAdded) {
|
||||
current.Append(line.Substring(1));
|
||||
} else {
|
||||
rs.Add(current);
|
||||
|
||||
current = new Block();
|
||||
current.Side = Side.Right;
|
||||
current.Mode = LineMode.Added;
|
||||
current.RightStart = right;
|
||||
current.Append(line.Substring(1));
|
||||
}
|
||||
|
||||
right++;
|
||||
} else if (line[0] == '\\') {
|
||||
var tmp = new Block();
|
||||
tmp.Side = current.Side;
|
||||
tmp.Mode = LineMode.Indicator;
|
||||
tmp.Append(line.Substring(1));
|
||||
|
||||
rs.Add(current);
|
||||
rs.Add(tmp);
|
||||
rs.Fit();
|
||||
|
||||
current = new Block();
|
||||
current.LeftStart = left;
|
||||
current.RightStart = right;
|
||||
} else {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (match.Success) {
|
||||
rs.Add(current);
|
||||
rs.Fit();
|
||||
|
||||
left = int.Parse(match.Groups[1].Value);
|
||||
right = int.Parse(match.Groups[2].Value);
|
||||
|
||||
current = new Block();
|
||||
current.Mode = LineMode.Indicator;
|
||||
current.Append(line);
|
||||
} else {
|
||||
if (current.IsBothSideNormal) {
|
||||
current.Append(line.Substring(1));
|
||||
} else {
|
||||
rs.Add(current);
|
||||
rs.Fit();
|
||||
|
||||
current = new Block();
|
||||
current.LeftStart = left;
|
||||
current.RightStart = right;
|
||||
current.Append(line.Substring(1));
|
||||
}
|
||||
|
||||
left++;
|
||||
right++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rs.Add(current);
|
||||
rs.Fit();
|
||||
|
||||
if (rs.IsBinary) rs.Blocks.Clear();
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get file size changes for binary file.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="revisions"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="orgPath"></param>
|
||||
/// <returns></returns>
|
||||
public static BinaryChange GetSizeChange(Repository repo, string[] revisions, string path, string orgPath = null) {
|
||||
var change = new BinaryChange();
|
||||
|
||||
if (revisions.Length == 0) { // Compare working copy with HEAD
|
||||
change.Size = new FileInfo(Path.Combine(repo.Path, path)).Length;
|
||||
change.PreSize = repo.GetFileSize("HEAD", path);
|
||||
} else if (revisions.Length == 1) { // Compare HEAD with given revision.
|
||||
change.Size = repo.GetFileSize("HEAD", path);
|
||||
if (!string.IsNullOrEmpty(orgPath)) {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], orgPath);
|
||||
} else {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], path);
|
||||
}
|
||||
} else {
|
||||
change.Size = repo.GetFileSize(revisions[1], path);
|
||||
if (!string.IsNullOrEmpty(orgPath)) {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], orgPath);
|
||||
} else {
|
||||
change.PreSize = repo.GetFileSize(revisions[0], path);
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
202
src/Git/MergeTool.cs
Normal file
202
src/Git/MergeTool.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
300
src/Git/Preference.cs
Normal file
300
src/Git/Preference.cs
Normal file
|
@ -0,0 +1,300 @@
|
|||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
var stream = new FileStream(SAVE_PATH, FileMode.Open);
|
||||
var reader = new XmlSerializer(typeof(Preference));
|
||||
instance = (Preference)reader.Deserialize(stream);
|
||||
stream.Close();
|
||||
} catch {
|
||||
instance = new Preference();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
src/Git/Remote.cs
Normal file
97
src/Git/Remote.cs
Normal 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(new Remote() { Name = name }, true, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete remote.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="remote"></param>
|
||||
public static void Delete(Repository repo, string remote) {
|
||||
var errs = repo.RunCommand($"remote remove {remote}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edit remote.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="url"></param>
|
||||
public void Edit(Repository repo, string name, string url) {
|
||||
string errs = null;
|
||||
|
||||
if (name != Name) {
|
||||
errs = repo.RunCommand($"remote rename {Name} {name}", null);
|
||||
if (errs != null) {
|
||||
App.RaiseError(errs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (url != URL) {
|
||||
errs = repo.RunCommand($"remote set-url {name} {url}", null);
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1163
src/Git/Repository.cs
Normal file
1163
src/Git/Repository.cs
Normal file
File diff suppressed because it is too large
Load diff
101
src/Git/Stash.cs
Normal file
101
src/Git/Stash.cs
Normal 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
src/Git/Tag.cs
Normal file
118
src/Git/Tag.cs
Normal 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
src/Git/User.cs
Normal file
42
src/Git/User.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue