mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-23 21:24:59 +00:00
Upgrade project style
This commit is contained in:
parent
afd22ca85d
commit
baa6c1445a
136 changed files with 29 additions and 770 deletions
35
SourceGit/Git/Blame.cs
Normal file
35
SourceGit/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
SourceGit/Git/Branch.cs
Normal file
190
SourceGit/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; }
|
||||
|
||||
/// <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
147
SourceGit/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
262
SourceGit/Git/Commit.cs
Normal file
262
SourceGit/Git/Commit.cs
Normal 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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
SourceGit/Git/Decorator.cs
Normal file
21
SourceGit/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; }
|
||||
}
|
||||
}
|
202
SourceGit/Git/MergeTool.cs
Normal file
202
SourceGit/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;
|
||||
}
|
||||
}
|
||||
}
|
296
SourceGit/Git/Preference.cs
Normal file
296
SourceGit/Git/Preference.cs
Normal 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
97
SourceGit/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(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
1058
SourceGit/Git/Repository.cs
Normal file
File diff suppressed because it is too large
Load diff
101
SourceGit/Git/Stash.cs
Normal file
101
SourceGit/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
SourceGit/Git/Tag.cs
Normal file
118
SourceGit/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
SourceGit/Git/User.cs
Normal file
42
SourceGit/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