refactor<*>: rewrite all with AvaloniaUI

This commit is contained in:
leo 2024-02-06 15:08:37 +08:00
parent 0136904612
commit 2a62596999
521 changed files with 19780 additions and 23244 deletions

View file

@ -1,27 +1,24 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands {
/// <summary>
/// `git add`命令
/// </summary>
public class Add : Command {
public Add(string repo) {
Cwd = repo;
Args = "add .";
}
public Add(string repo, List<Models.Change> changes = null) {
WorkingDirectory = repo;
Context = repo;
public Add(string repo, List<string> paths) {
StringBuilder builder = new StringBuilder();
builder.Append("add --");
foreach (var p in paths) {
builder.Append(" \"");
builder.Append(p);
builder.Append("\"");
if (changes == null || changes.Count == 0) {
Args = "add .";
} else {
var builder = new StringBuilder();
builder.Append("add --");
foreach (var c in changes) {
builder.Append(" \"");
builder.Append(c.Path);
builder.Append("\"");
}
Args = builder.ToString();
}
Cwd = repo;
Args = builder.ToString();
}
}
}

View file

@ -1,11 +1,8 @@
namespace SourceGit.Commands {
/// <summary>
/// 应用Patch
/// </summary>
namespace SourceGit.Commands {
public class Apply : Command {
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = "apply ";
if (ignoreWhitespace) Args += "--ignore-whitespace ";
else Args += $"--whitespace={whitespaceMode} ";

View file

@ -1,22 +1,19 @@
using System;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 存档命令
/// </summary>
public class Archive : Command {
private Action<string> handler;
public Archive(string repo, string revision, string to, Action<string> onProgress) {
Cwd = repo;
Args = $"archive --format=zip --verbose --output=\"{to}\" {revision}";
public Archive(string repo, string revision, string saveTo, Action<string> outputHandler) {
WorkingDirectory = repo;
Context = repo;
Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}";
TraitErrorAsOutput = true;
handler = onProgress;
_outputHandler = outputHandler;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -2,59 +2,59 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 查看、添加或移除忽略变更文件
/// </summary>
public class AssumeUnchanged {
private string repo;
class ViewCommand : Command {
private static readonly Regex REG = new Regex(@"^(\w)\s+(.+)$");
private List<string> outs = new List<string>();
public ViewCommand(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Args = "ls-files -v";
RaiseError = false;
}
public List<string> Result() {
Exec();
return outs;
return _outs;
}
public override void OnReadline(string line) {
protected override void OnReadline(string line) {
var match = REG.Match(line);
if (!match.Success) return;
if (match.Groups[1].Value == "h") {
outs.Add(match.Groups[2].Value);
_outs.Add(match.Groups[2].Value);
}
}
private List<string> _outs = new List<string>();
}
class ModCommand : Command {
public ModCommand(string repo, string file, bool bAdd) {
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = $"update-index {mode} -- \"{file}\"";
}
}
public AssumeUnchanged(string repo) {
this.repo = repo;
_repo = repo;
}
public List<string> View() {
return new ViewCommand(repo).Result();
return new ViewCommand(_repo).Result();
}
public void Add(string file) {
new ModCommand(repo, file, true).Exec();
new ModCommand(_repo, file, true).Exec();
}
public void Remove(string file) {
new ModCommand(repo, file, false).Exec();
new ModCommand(_repo, file, false).Exec();
}
private string _repo;
}
}

View file

@ -1,77 +1,90 @@
using System;
using System.Collections.Generic;
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 逐行追溯
/// </summary>
public class Blame : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)");
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
private Data data = new Data();
private bool needUnifyCommitSHA = false;
private int minSHALen = 0;
public class Data {
public List<Models.BlameLine> Lines = new List<Models.BlameLine>();
public bool IsBinary = false;
}
public Blame(string repo, string file, string revision) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = $"blame -t {revision} -- \"{file}\"";
RaiseError = false;
_result.File = file;
}
public Data Result() {
Exec();
public Models.BlameData Result() {
var succ = Exec();
if (!succ) {
return new Models.BlameData();
}
if (needUnifyCommitSHA) {
foreach (var line in data.Lines) {
if (line.CommitSHA.Length > minSHALen) {
line.CommitSHA = line.CommitSHA.Substring(0, minSHALen);
if (_needUnifyCommitSHA) {
foreach (var line in _result.LineInfos) {
if (line.CommitSHA.Length > _minSHALen) {
line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen);
}
}
}
return data;
_result.Content = _content.ToString();
return _result;
}
public override void OnReadline(string line) {
if (data.IsBinary) return;
protected override void OnReadline(string line) {
if (_result.IsBinary) return;
if (string.IsNullOrEmpty(line)) return;
if (line.IndexOf('\0') >= 0) {
data.IsBinary = true;
data.Lines.Clear();
_result.IsBinary = true;
_result.LineInfos.Clear();
return;
}
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
_content.AppendLine(match.Groups[4].Value);
var commit = match.Groups[1].Value;
var author = match.Groups[2].Value;
var timestamp = int.Parse(match.Groups[3].Value);
var content = match.Groups[4].Value;
var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd");
if (commit == _lastSHA) {
var info = new Models.BlameLineInfo() {
CommitSHA = commit,
Author = string.Empty,
Time = string.Empty,
};
var blameLine = new Models.BlameLine() {
LineNumber = $"{data.Lines.Count + 1}",
CommitSHA = commit,
Author = author,
Time = when,
Content = content,
};
_result.LineInfos.Add(info);
} else {
var author = match.Groups[2].Value;
var timestamp = int.Parse(match.Groups[3].Value);
var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd");
if (line[0] == '^') {
needUnifyCommitSHA = true;
if (minSHALen == 0) minSHALen = commit.Length;
else if (commit.Length < minSHALen) minSHALen = commit.Length;
var blameLine = new Models.BlameLineInfo() {
IsFirstInGroup = true,
CommitSHA = commit,
Author = author,
Time = when,
};
_lastSHA = commit;
_result.LineInfos.Add(blameLine);
}
data.Lines.Add(blameLine);
if (line[0] == '^') {
_needUnifyCommitSHA = true;
_minSHALen = Math.Min(_minSHALen, commit.Length);
}
}
private Models.BlameData _result = new Models.BlameData();
private StringBuilder _content = new StringBuilder();
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;
}
}

View file

@ -1,38 +1,39 @@
namespace SourceGit.Commands {
/// <summary>
/// 分支相关操作
/// </summary>
class Branch : Command {
private string target = null;
public Branch(string repo, string branch) {
Cwd = repo;
target = branch;
namespace SourceGit.Commands {
public static class Branch {
public static bool Create(string repo, string name, string basedOn) {
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch {name} {basedOn}";
return cmd.Exec();
}
public void Create(string basedOn) {
Args = $"branch {target} {basedOn}";
Exec();
public static bool Rename(string repo, string name, string to) {
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -M {name} {to}";
return cmd.Exec();
}
public void Rename(string to) {
Args = $"branch -M {target} {to}";
Exec();
}
public void SetUpstream(string upstream) {
Args = $"branch {target} ";
public static bool SetUpstream(string repo, string name, string upstream) {
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
if (string.IsNullOrEmpty(upstream)) {
Args += "--unset-upstream";
cmd.Args = $"branch {name} --unset-upstream";
} else {
Args += $"-u {upstream}";
cmd.Args = $"branch {name} -u {upstream}";
}
Exec();
return cmd.Exec();
}
public void Delete() {
Args = $"branch -D {target}";
Exec();
public static bool Delete(string repo, string name) {
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -D {name}";
return cmd.Exec();
}
}
}

View file

@ -1,29 +1,25 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands {
/// <summary>
/// 检出
/// </summary>
public class Checkout : Command {
private Action<string> handler = null;
public Checkout(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
}
public bool Branch(string branch, Action<string> onProgress) {
Args = $"checkout --progress {branch}";
TraitErrorAsOutput = true;
handler = onProgress;
_outputHandler = onProgress;
return Exec();
}
public bool Branch(string branch, string basedOn, Action<string> onProgress) {
Args = $"checkout --progress -b {branch} {basedOn}";
TraitErrorAsOutput = true;
handler = onProgress;
_outputHandler = onProgress;
return Exec();
}
@ -54,8 +50,10 @@ namespace SourceGit.Commands {
return Exec();
}
public override void OnReadline(string line) {
handler?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -1,12 +1,9 @@
namespace SourceGit.Commands {
/// <summary>
/// 遴选命令
/// </summary>
namespace SourceGit.Commands {
public class CherryPick : Command {
public CherryPick(string repo, string commit, bool noCommit) {
var mode = noCommit ? "-n" : "--ff";
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = $"cherry-pick {mode} {commit}";
}
}

View file

@ -1,14 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands {
/// <summary>
/// 清理指令
/// </summary>
public class Clean : Command {
public Clean(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = "clean -qfd";
}
@ -21,7 +18,8 @@ namespace SourceGit.Commands {
builder.Append("\"");
}
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = builder.ToString();
}
}

View file

@ -1,24 +1,18 @@
using System;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 克隆
/// </summary>
public class Clone : Command {
private Action<string> handler = null;
private Action<string> onError = null;
private Action<string> _notifyProgress;
public Clone(string path, string url, string localName, string sshKey, string extraArgs, Action<string> outputHandler, Action<string> errHandler) {
Cwd = path;
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler) {
Context = ctx;
WorkingDirectory = path;
TraitErrorAsOutput = true;
handler = outputHandler;
onError = errHandler;
if (string.IsNullOrEmpty(sshKey)) {
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
} else {
Args = "-c credential.helper=manager ";
} else {
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
Args += "clone --progress --verbose --recurse-submodules ";
@ -26,14 +20,12 @@ namespace SourceGit.Commands {
if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} ";
Args += $"{url} ";
if (!string.IsNullOrEmpty(localName)) Args += localName;
_notifyProgress = ouputHandler;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
}
public override void OnException(string message) {
onError?.Invoke(message);
protected override void OnReadline(string line) {
_notifyProgress?.Invoke(line);
}
}
}

View file

@ -1,3 +1,4 @@
using Avalonia.Threading;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -5,60 +6,27 @@ using System.Text;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 用于取消命令执行的上下文对象
/// </summary>
public class Context {
public bool IsCancelRequested { get; set; } = false;
}
/// <summary>
/// 命令接口
/// </summary>
public class Command {
private static readonly Regex PROGRESS_REG = new Regex(@"\d+%");
/// <summary>
/// 读取全部输出时的结果
/// </summary>
public class ReadToEndResult {
public bool IsSuccess { get; set; }
public string Output { get; set; }
public string Error { get; set; }
public class CancelToken {
public bool Requested { get; set; } = false;
}
/// <summary>
/// 上下文
/// </summary>
public Context Ctx { get; set; } = null;
public class ReadToEndResult {
public bool IsSuccess { get; set; }
public string StdOut { get; set; }
public string StdErr { get; set; }
}
/// <summary>
/// 运行路径
/// </summary>
public string Cwd { get; set; } = "";
/// <summary>
/// 参数
/// </summary>
public string Args { get; set; } = "";
/// <summary>
/// 是否忽略错误
/// </summary>
public bool DontRaiseError { get; set; } = false;
/// <summary>
/// 使用标准错误输出
/// </summary>
public string Context { get; set; } = string.Empty;
public CancelToken Cancel { get; set; } = null;
public string WorkingDirectory { get; set; } = null;
public string Args { get; set; } = string.Empty;
public bool RaiseError { get; set; } = true;
public bool TraitErrorAsOutput { get; set; } = false;
/// <summary>
/// 运行
/// </summary>
public bool Exec() {
var start = new ProcessStartInfo();
start.FileName = Models.Preference.Instance.Git.Path;
start.FileName = Native.OS.GitExecutableFile;
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
@ -67,49 +35,53 @@ namespace SourceGit.Commands {
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (o, e) => {
if (Ctx != null && Ctx.IsCancelRequested) {
proc.OutputDataReceived += (_, e) => {
if (Cancel != null && Cancel.Requested) {
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited) proc.Kill();
if (!proc.HasExited) proc.Kill(true);
return;
}
if (e.Data == null) return;
OnReadline(e.Data);
if (e.Data != null) OnReadline(e.Data);
};
proc.ErrorDataReceived += (o, e) => {
if (Ctx != null && Ctx.IsCancelRequested) {
proc.ErrorDataReceived += (_, e) => {
if (Cancel != null && Cancel.Requested) {
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited) proc.Kill();
if (!proc.HasExited) proc.Kill(true);
return;
}
if (string.IsNullOrEmpty(e.Data)) return;
if (TraitErrorAsOutput) OnReadline(e.Data);
// 错误信息中忽略进度相关的输出
// Ignore progress messages
if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal)) return;
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return;
if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) return;
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) return;
if (PROGRESS_REG.IsMatch(e.Data)) return;
if (_progressRegex.IsMatch(e.Data)) return;
errs.Add(e.Data);
};
try {
proc.Start();
} catch (Exception e) {
if (!DontRaiseError) OnException(e.Message);
if (RaiseError) {
Dispatcher.UIThread.Invoke(() => {
App.RaiseException(Context, e.Message);
});
}
return false;
}
@ -121,19 +93,20 @@ namespace SourceGit.Commands {
proc.Close();
if (!isCancelled && exitCode != 0 && errs.Count > 0) {
if (!DontRaiseError) OnException(string.Join("\n", errs));
if (RaiseError) {
Dispatcher.UIThread.Invoke(() => {
App.RaiseException(Context, string.Join("\n", errs));
});
}
return false;
} else {
return true;
}
}
/// <summary>
/// 直接读取全部标准输出
/// </summary>
public ReadToEndResult ReadToEnd() {
var start = new ProcessStartInfo();
start.FileName = Models.Preference.Instance.Git.Path;
start.FileName = Native.OS.GitExecutableFile;
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
@ -142,22 +115,23 @@ namespace SourceGit.Commands {
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd;
if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory;
var proc = new Process() { StartInfo = start };
try {
proc.Start();
} catch (Exception e) {
return new ReadToEndResult() {
Output = string.Empty,
Error = e.Message,
IsSuccess = false,
StdOut = string.Empty,
StdErr = e.Message,
};
}
var rs = new ReadToEndResult();
rs.Output = proc.StandardOutput.ReadToEnd();
rs.Error = proc.StandardError.ReadToEnd();
var rs = new ReadToEndResult() {
StdOut = proc.StandardOutput.ReadToEnd(),
StdErr = proc.StandardError.ReadToEnd(),
};
proc.WaitForExit();
rs.IsSuccess = proc.ExitCode == 0;
@ -166,19 +140,8 @@ namespace SourceGit.Commands {
return rs;
}
/// <summary>
/// 调用Exec时的读取函数
/// </summary>
/// <param name="line"></param>
public virtual void OnReadline(string line) {
}
protected virtual void OnReadline(string line) { }
/// <summary>
/// 默认异常处理函数
/// </summary>
/// <param name="message"></param>
public virtual void OnException(string message) {
App.Exception(Cwd, message);
}
private static readonly Regex _progressRegex = new Regex(@"\d+%");
}
}
}

View file

@ -1,17 +1,16 @@
using System.IO;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// `git commit`命令
/// </summary>
public class Commit : Command {
public Commit(string repo, string message, bool amend) {
public Commit(string repo, string message, bool amend, bool allowEmpty = false) {
var file = Path.GetTempFileName();
File.WriteAllText(file, message);
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = $"commit --file=\"{file}\"";
if (amend) Args += " --amend --no-edit";
if (allowEmpty) Args += " --allow-empty";
}
}
}

View file

@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 取得一个提交的变更列表
/// </summary>
public class CommitChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private List<Models.Change> changes = new List<Models.Change>();
public CommitChanges(string cwd, string commit) {
Cwd = cwd;
Args = $"show --name-status {commit}";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0]) {
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
}
}
}
}

View file

@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 对比两个提交间的变更
/// </summary>
public class CommitRangeChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private List<Models.Change> changes = new List<Models.Change>();
public CommitRangeChanges(string cwd, string start, string end) {
Cwd = cwd;
Args = $"diff --name-status {start} {end}";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0]) {
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
}
}
}
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class CompareRevisions : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
public CompareRevisions(string repo, string start, string end) {
WorkingDirectory = repo;
Context = repo;
Args = $"diff --name-status {start} {end}";
}
public List<Models.Change> Result() {
Exec();
_changes.Sort((l, r) => l.Path.CompareTo(r.Path));
return _changes;
}
protected override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0]) {
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
case 'R': change.Set(Models.ChangeState.Renamed); _changes.Add(change); break;
case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); break;
}
}
private List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -1,32 +1,55 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands {
/// <summary>
/// config命令
/// </summary>
public class Config : Command {
public class Config : Command {
public Config(string repository) {
WorkingDirectory = repository;
Context = repository;
RaiseError = false;
}
public Config() { }
public Dictionary<string, string> ListAll() {
Args = "config -l";
public Config(string repo) {
Cwd = repo;
var output = ReadToEnd();
var rs = new Dictionary<string, string>();
if (output.IsSuccess) {
var lines = output.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) {
var idx = line.IndexOf('=');
if (idx != -1) {
var key = line.Substring(0, idx).Trim();
var val = line.Substring(idx+1).Trim();
if (rs.ContainsKey(key)) {
rs[key] = val;
} else {
rs.Add(key, val);
}
}
}
}
return rs;
}
public string Get(string key) {
Args = $"config {key}";
return ReadToEnd().Output.Trim();
return ReadToEnd().StdOut.Trim();
}
public bool Set(string key, string val, bool allowEmpty = false) {
if (!allowEmpty && string.IsNullOrEmpty(val)) {
if (string.IsNullOrEmpty(Cwd)) {
public bool Set(string key, string value, bool allowEmpty = false) {
if (!allowEmpty && string.IsNullOrWhiteSpace(value)) {
if (string.IsNullOrEmpty(WorkingDirectory)) {
Args = $"config --global --unset {key}";
} else {
Args = $"config --unset {key}";
}
} else {
if (string.IsNullOrEmpty(Cwd)) {
Args = $"config --global {key} \"{val}\"";
if (string.IsNullOrWhiteSpace(WorkingDirectory)) {
Args = $"config --global {key} \"{value}\"";
} else {
Args = $"config {key} \"{val}\"";
Args = $"config {key} \"{value}\"";
}
}

View file

@ -1,111 +1,147 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// Diff命令用于文件文件比对
/// </summary>
public class Diff : Command {
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
private Models.TextChanges changes = new Models.TextChanges();
private List<Models.TextChanges.Line> deleted = new List<Models.TextChanges.Line>();
private List<Models.TextChanges.Line> added = new List<Models.TextChanges.Line>();
private int oldLine = 0;
private int newLine = 0;
private int lineIndex = 0;
private static readonly string PREFIX_LFS = " version https://git-lfs.github.com/spec/";
public Diff(string repo, string args) {
Cwd = repo;
Args = $"diff --ignore-cr-at-eol --unified=4 {args}";
public Diff(string repo, Models.DiffOption opt) {
WorkingDirectory = repo;
Context = repo;
Args = $"diff --ignore-cr-at-eol --unified=4 {opt}";
}
public Models.TextChanges Result() {
public Models.DiffResult Result() {
Exec();
ProcessChanges();
if (changes.IsBinary) changes.Lines.Clear();
lineIndex = 0;
return changes;
if (_result.IsBinary || _result.IsLFS) {
_result.TextDiff = null;
} else {
ProcessInlineHighlights();
if (_result.TextDiff.Lines.Count == 0) {
_result.TextDiff = null;
} else {
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
}
return _result;
}
public override void OnReadline(string line) {
if (changes.IsBinary) return;
protected override void OnReadline(string line) {
if (_result.IsBinary) return;
if (changes.Lines.Count == 0) {
if (_result.IsLFS) {
var ch = line[0];
if (ch == '-') {
line = line.Substring(1);
if (line.StartsWith("oid sha256:")) {
_result.LFSDiff.Old.Oid = line.Substring(11);
} else if (line.StartsWith("size ")) {
_result.LFSDiff.Old.Size = long.Parse(line.Substring(5));
}
} else if (ch == '+') {
line = line.Substring(1);
if (line.StartsWith("oid sha256:")) {
_result.LFSDiff.New.Oid = line.Substring(11);
} else if (line.StartsWith("size ")) {
_result.LFSDiff.New.Size = long.Parse(line.Substring(5));
}
} else if (line.StartsWith(" size ")) {
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
}
return;
}
if (_result.TextDiff.Lines.Count == 0) {
var match = REG_INDICATOR.Match(line);
if (!match.Success) {
if (line.StartsWith("Binary", StringComparison.Ordinal)) changes.IsBinary = true;
if (line.StartsWith("Binary", StringComparison.Ordinal)) _result.IsBinary = true;
return;
}
oldLine = int.Parse(match.Groups[1].Value);
newLine = int.Parse(match.Groups[2].Value);
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Indicator, line, "", ""));
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, "", ""));
} else {
if (line.Length == 0) {
ProcessChanges();
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Normal, "", $"{oldLine}", $"{newLine}"));
oldLine++;
newLine++;
ProcessInlineHighlights();
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", $"{_oldLine}", $"{_newLine}"));
_oldLine++;
_newLine++;
return;
}
var ch = line[0];
if (ch == '-') {
deleted.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", ""));
oldLine++;
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), $"{_oldLine}", ""));
_oldLine++;
} else if (ch == '+') {
added.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}"));
newLine++;
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), "", $"{_newLine}"));
_newLine++;
} else if (ch != '\\') {
ProcessChanges();
ProcessInlineHighlights();
var match = REG_INDICATOR.Match(line);
if (match.Success) {
oldLine = int.Parse(match.Groups[1].Value);
newLine = int.Parse(match.Groups[2].Value);
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Indicator, line, "", ""));
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, "", ""));
} else {
changes.Lines.Add(new Models.TextChanges.Line(lineIndex++, Models.TextChanges.LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}"));
oldLine++;
newLine++;
if (line.StartsWith(PREFIX_LFS)) {
_result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff();
return;
}
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), $"{_oldLine}", $"{_newLine}"));
_oldLine++;
_newLine++;
}
}
}
}
private void ProcessChanges() {
if (deleted.Any()) {
if (added.Count == deleted.Count) {
for (int i = added.Count - 1; i >= 0; i--) {
var left = deleted[i];
var right = added[i];
private void ProcessInlineHighlights() {
if (_deleted.Count > 0) {
if (_added.Count == _deleted.Count) {
for (int i = _added.Count - 1; i >= 0; i--) {
var left = _deleted[i];
var right = _added[i];
if (left.Content.Length > 1024 || right.Content.Length > 1024) continue;
var chunks = Models.TextCompare.Process(left.Content, right.Content);
var chunks = Models.TextInlineChange.Compare(left.Content, right.Content);
if (chunks.Count > 4) continue;
foreach (var chunk in chunks) {
if (chunk.DeletedCount > 0) {
left.Highlights.Add(new Models.TextChanges.HighlightRange(chunk.DeletedStart, chunk.DeletedCount));
left.Highlights.Add(new Models.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount));
}
if (chunk.AddedCount > 0) {
right.Highlights.Add(new Models.TextChanges.HighlightRange(chunk.AddedStart, chunk.AddedCount));
right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount));
}
}
}
}
changes.Lines.AddRange(deleted);
deleted.Clear();
_result.TextDiff.Lines.AddRange(_deleted);
_deleted.Clear();
}
if (added.Any()) {
changes.Lines.AddRange(added);
added.Clear();
if (_added.Count > 0) {
_result.TextDiff.Lines.AddRange(_added);
_added.Clear();
}
}
private Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() };
private List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private int _oldLine = 0;
private int _newLine = 0;
}
}

View file

@ -1,28 +1,19 @@
using System;
using System;
using System.Collections.Generic;
namespace SourceGit.Commands {
/// <summary>
/// 忽略变更
/// </summary>
public class Discard {
private string repo = null;
public Discard(string repo) {
this.repo = repo;
}
public void Whole() {
public static class Discard {
public static void All(string repo) {
new Reset(repo, "HEAD", "--hard").Exec();
new Clean(repo).Exec();
}
public void Changes(List<Models.Change> changes) {
public static void Changes(string repo, List<Models.Change> changes) {
var needClean = new List<string>();
var needCheckout = new List<string>();
foreach (var c in changes) {
if (c.WorkTree == Models.Change.Status.Untracked || c.WorkTree == Models.Change.Status.Added) {
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) {
needClean.Add(c.Path);
} else {
needCheckout.Add(c.Path);

View file

@ -1,17 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 拉取
/// </summary>
public class Fetch : Command {
private Action<string> handler = null;
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler) {
Cwd = repo;
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
@ -24,12 +18,12 @@ namespace SourceGit.Commands {
Args += "fetch --progress --verbose ";
if (prune) Args += "--prune ";
Args += remote;
handler = outputHandler;
AutoFetch.MarkFetched(repo);
}
public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action<string> outputHandler) {
Cwd = repo;
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
@ -40,63 +34,12 @@ namespace SourceGit.Commands {
}
Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}";
handler = outputHandler;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
}
}
/// <summary>
/// 自动拉取每隔10分钟
/// </summary>
public class AutoFetch {
private static Dictionary<string, AutoFetch> jobs = new Dictionary<string, AutoFetch>();
private Fetch cmd = null;
private long nextFetchPoint = 0;
private Timer timer = null;
public static void Start(string repo) {
if (!Models.Preference.Instance.Git.AutoFetchRemotes) return;
// 只自动更新加入管理列表中的仓库(子模块等不自动更新)
var exists = Models.Preference.Instance.FindRepository(repo);
if (exists == null) return;
var job = new AutoFetch(repo);
jobs.Add(repo, job);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
public static void MarkFetched(string repo) {
if (!jobs.ContainsKey(repo)) return;
jobs[repo].nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
}
public static void Stop(string repo) {
if (!jobs.ContainsKey(repo)) return;
jobs[repo].timer.Dispose();
jobs.Remove(repo);
}
public AutoFetch(string repo) {
cmd = new Fetch(repo, "--all", true, null);
cmd.DontRaiseError = true;
nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
timer = new Timer(OnTick, null, 60000, 10000);
}
private void OnTick(object o) {
var now = DateTime.Now.ToFileTime();
if (nextFetchPoint > now) return;
Models.Watcher.SetEnabled(cmd.Cwd, false);
cmd.Exec();
nextFetchPoint = DateTime.Now.AddMinutes(10).ToFileTime();
Models.Watcher.SetEnabled(cmd.Cwd, true);
}
private Action<string> _outputHandler;
}
}

View file

@ -1,12 +1,9 @@
namespace SourceGit.Commands {
/// <summary>
/// 将Commit另存为Patch文件
/// </summary>
namespace SourceGit.Commands {
public class FormatPatch : Command {
public FormatPatch(string repo, string commit, string path) {
Cwd = repo;
Args = $"format-patch {commit} -1 -o \"{path}\"";
public FormatPatch(string repo, string commit, string saveTo) {
WorkingDirectory = repo;
Context = repo;
Args = $"format-patch {commit} -1 -o \"{saveTo}\"";
}
}
}

View file

@ -1,21 +1,19 @@
using System;
namespace SourceGit.Commands {
/// <summary>
/// GC
/// </summary>
public class GC : Command {
private Action<string> handler;
public GC(string repo, Action<string> onProgress) {
Cwd = repo;
Args = "gc";
public GC(string repo, Action<string> outputHandler) {
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
handler = onProgress;
Args = "gc";
}
public override void OnReadline(string line) {
handler?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -1,17 +0,0 @@
namespace SourceGit.Commands {
/// <summary>
/// 取得一个库的根路径
/// </summary>
public class GetRepositoryRootPath : Command {
public GetRepositoryRootPath(string path) {
Cwd = path;
Args = "rev-parse --show-toplevel";
}
public string Result() {
var rs = ReadToEnd().Output;
if (string.IsNullOrEmpty(rs)) return null;
return rs.Trim();
}
}
}

View file

@ -1,24 +1,22 @@
namespace SourceGit.Commands {
/// <summary>
/// Git-Flow命令
/// </summary>
public class GitFlow : Command {
using System.Collections.Generic;
namespace SourceGit.Commands {
public class GitFlow : Command {
public GitFlow(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
}
public bool Init(string master, string develop, string feature, string release, string hotfix, string version) {
var branches = new Branches(Cwd).Result();
public bool Init(List<Models.Branch> branches, string master, string develop, string feature, string release, string hotfix, string version) {
var current = branches.Find(x => x.IsCurrent);
var masterBranch = branches.Find(x => x.Name == master);
if (masterBranch == null && current != null) new Branch(Cwd, develop).Create(current.Head);
if (masterBranch == null && current != null) Branch.Create(WorkingDirectory, master, current.Head);
var devBranch = branches.Find(x => x.Name == develop);
if (devBranch == null && current != null) new Branch(Cwd, develop).Create(current.Head);
if (devBranch == null && current != null) Branch.Create(WorkingDirectory, develop, current.Head);
var cmd = new Config(Cwd);
var cmd = new Config(WorkingDirectory);
cmd.Set("gitflow.branch.master", master);
cmd.Set("gitflow.branch.develop", develop);
cmd.Set("gitflow.prefix.feature", feature);
@ -32,7 +30,7 @@ namespace SourceGit.Commands {
return Exec();
}
public void Start(Models.GitFlowBranchType type, string name) {
public bool Start(Models.GitFlowBranchType type, string name) {
switch (type) {
case Models.GitFlowBranchType.Feature:
Args = $"flow feature start {name}";
@ -44,13 +42,14 @@ namespace SourceGit.Commands {
Args = $"flow hotfix start {name}";
break;
default:
return;
App.RaiseException(Context, "Bad branch type!!!");
return false;
}
Exec();
return Exec();
}
public void Finish(Models.GitFlowBranchType type, string name, bool keepBranch) {
public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch) {
var option = keepBranch ? "-k" : string.Empty;
switch (type) {
case Models.GitFlowBranchType.Feature:
@ -63,10 +62,11 @@ namespace SourceGit.Commands {
Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\"";
break;
default:
return;
App.RaiseException(Context, "Bad branch type!!!");
return false;
}
Exec();
return Exec();
}
}
}

View file

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

18
src/Commands/IsBinary.cs Normal file
View file

@ -0,0 +1,18 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class IsBinary : Command {
private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$");
public IsBinary(string repo, string commit, string path) {
WorkingDirectory = repo;
Context = repo;
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
RaiseError = false;
}
public bool Result() {
return REG_TEST.IsMatch(ReadToEnd().StdOut);
}
}
}

View file

@ -1,18 +0,0 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 查询指定版本下的某文件是否是二进制文件
/// </summary>
public class IsBinaryFile : Command {
private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$");
public IsBinaryFile(string repo, string commit, string path) {
Cwd = repo;
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
}
public bool Result() {
return REG_TEST.IsMatch(ReadToEnd().Output);
}
}
}

View file

@ -0,0 +1,15 @@
namespace SourceGit.Commands {
public class IsLFSFiltered : Command {
public IsLFSFiltered(string repo, string path) {
WorkingDirectory = repo;
Context = repo;
Args = $"check-attr -a -z \"{path}\"";
RaiseError = false;
}
public bool Result() {
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Contains("filter\0lfs");
}
}
}

View file

@ -1,51 +1,40 @@
using System;
using System;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// LFS相关
/// </summary>
public class LFS {
private string repo;
private class PruneCmd : Command {
private Action<string> handler;
class PruneCmd : Command {
public PruneCmd(string repo, Action<string> onProgress) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = "lfs prune";
TraitErrorAsOutput = true;
handler = onProgress;
_outputHandler = onProgress;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
public LFS(string repo) {
this.repo = repo;
_repo = repo;
}
public bool IsEnabled() {
var path = Path.Combine(repo, ".git", "hooks", "pre-push");
var path = Path.Combine(_repo, ".git", "hooks", "pre-push");
if (!File.Exists(path)) return false;
var content = File.ReadAllText(path);
return content.Contains("git lfs pre-push");
}
public bool IsFiltered(string path) {
var cmd = new Command();
cmd.Cwd = repo;
cmd.Args = $"check-attr -a -z \"{path}\"";
var rs = cmd.ReadToEnd();
return rs.Output.Contains("filter\0lfs");
public void Prune(Action<string> outputHandler) {
new PruneCmd(_repo, outputHandler).Exec();
}
public void Prune(Action<string> onProgress) {
new PruneCmd(repo, onProgress).Exec();
}
private string _repo;
}
}

View file

@ -1,67 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 取得本地工作副本变更
/// </summary>
public class LocalChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private static readonly string[] UNTRACKED = new string[] { "no", "all" };
private List<Models.Change> changes = new List<Models.Change>();
public LocalChanges(string path, bool includeUntracked = true) {
Cwd = path;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
if (line.EndsWith("/", StringComparison.Ordinal)) return; // Ignore changes with git-worktree
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status) {
case " M": change.Set(Models.Change.Status.None, Models.Change.Status.Modified); break;
case " A": change.Set(Models.Change.Status.None, Models.Change.Status.Added); break;
case " D": change.Set(Models.Change.Status.None, Models.Change.Status.Deleted); break;
case " R": change.Set(Models.Change.Status.None, Models.Change.Status.Renamed); break;
case " C": change.Set(Models.Change.Status.None, Models.Change.Status.Copied); break;
case "M": change.Set(Models.Change.Status.Modified, Models.Change.Status.None); break;
case "MM": change.Set(Models.Change.Status.Modified, Models.Change.Status.Modified); break;
case "MD": change.Set(Models.Change.Status.Modified, Models.Change.Status.Deleted); break;
case "A": change.Set(Models.Change.Status.Added, Models.Change.Status.None); break;
case "AM": change.Set(Models.Change.Status.Added, Models.Change.Status.Modified); break;
case "AD": change.Set(Models.Change.Status.Added, Models.Change.Status.Deleted); break;
case "D": change.Set(Models.Change.Status.Deleted, Models.Change.Status.None); break;
case "R": change.Set(Models.Change.Status.Renamed, Models.Change.Status.None); break;
case "RM": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Modified); break;
case "RD": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Deleted); break;
case "C": change.Set(Models.Change.Status.Copied, Models.Change.Status.None); break;
case "CM": change.Set(Models.Change.Status.Copied, Models.Change.Status.Modified); break;
case "CD": change.Set(Models.Change.Status.Copied, Models.Change.Status.Deleted); break;
case "DR": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Renamed); break;
case "DC": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Copied); break;
case "DD": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Deleted); break;
case "AU": change.Set(Models.Change.Status.Added, Models.Change.Status.Unmerged); break;
case "UD": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Deleted); break;
case "UA": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Added); break;
case "DU": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Unmerged); break;
case "AA": change.Set(Models.Change.Status.Added, Models.Change.Status.Added); break;
case "UU": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Unmerged); break;
case "??": change.Set(Models.Change.Status.Untracked, Models.Change.Status.Untracked); break;
default: return;
}
changes.Add(change);
}
}
}

View file

@ -1,21 +1,19 @@
using System;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 合并分支
/// </summary>
public class Merge : Command {
private Action<string> handler = null;
public Merge(string repo, string source, string mode, Action<string> onProgress) {
Cwd = repo;
Args = $"merge --progress {source} {mode}";
public Merge(string repo, string source, string mode, Action<string> outputHandler) {
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
handler = onProgress;
Args = $"merge --progress {source} {mode}";
}
public override void OnReadline(string line) {
handler?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler = null;
}
}

41
src/Commands/MergeTool.cs Normal file
View file

@ -0,0 +1,41 @@
using System.IO;
namespace SourceGit.Commands {
public static class MergeTool {
public static bool OpenForMerge(string repo, string tool, string mergeCmd, string file) {
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(mergeCmd)) {
App.RaiseException(repo, "Invalid external merge tool settings!");
return false;
}
if (!File.Exists(tool)) {
App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!");
return false;
}
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.RaiseError = false;
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{tool}\\\" {mergeCmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\"";
return cmd.Exec();
}
public static bool OpenForDiff(string repo, string tool, string diffCmd, Models.DiffOption option) {
if (string.IsNullOrWhiteSpace(tool) || string.IsNullOrWhiteSpace(diffCmd)) {
App.RaiseException(repo, "Invalid external merge tool settings!");
return false;
}
if (!File.Exists(tool)) {
App.RaiseException(repo, $"Can NOT found external merge tool in '{tool}'!");
return false;
}
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.RaiseError = false;
cmd.Args = $"-c difftool.sourcegit.cmd=\"\\\"{tool}\\\" {diffCmd}\" difftool --tool=sourcegit --no-prompt {option}";
return cmd.Exec();
}
}
}

View file

@ -1,19 +1,12 @@
using System;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 拉回
/// </summary>
public class Pull : Command {
private Action<string> handler = null;
private bool needStash = false;
public Pull(string repo, string remote, string branch, bool useRebase, bool autoStash, Action<string> onProgress) {
Cwd = repo;
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler) {
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
handler = onProgress;
needStash = autoStash;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
@ -27,25 +20,10 @@ namespace SourceGit.Commands {
Args += $"{remote} {branch}";
}
public bool Run() {
if (needStash) {
var changes = new LocalChanges(Cwd).Result();
if (changes.Count > 0) {
if (!new Stash(Cwd).Push(changes, "PULL_AUTO_STASH", true)) {
return false;
}
} else {
needStash = false;
}
}
var succ = Exec();
if (succ && needStash) new Stash(Cwd).Pop("stash@{0}");
return succ;
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
public override void OnReadline(string line) {
handler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -1,16 +1,12 @@
using System;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 推送
/// </summary>
public class Push : Command {
private Action<string> handler = null;
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
handler = onProgress;
_outputHandler = onProgress;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
@ -28,8 +24,16 @@ namespace SourceGit.Commands {
Args += $"{remote} {local}:{remoteBranch}";
}
/// <summary>
/// Only used to delete a remote branch!!!!!!
/// </summary>
/// <param name="repo"></param>
/// <param name="remote"></param>
/// <param name="branch"></param>
public Push(string repo, string remote, string branch) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
@ -42,7 +46,8 @@ namespace SourceGit.Commands {
}
public Push(string repo, string remote, string tag, bool isDelete) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey)) {
@ -56,8 +61,10 @@ namespace SourceGit.Commands {
Args += $"{remote} refs/tags/{tag}";
}
public override void OnReadline(string line) {
handler?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler = null;
}
}

View file

@ -1,31 +1,26 @@
using System;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 解析所有的分支
/// </summary>
public class Branches : Command {
public class QueryBranches : Command {
private static readonly string PREFIX_LOCAL = "refs/heads/";
private static readonly string PREFIX_REMOTE = "refs/remotes/";
private static readonly string CMD = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)\"";
private static readonly Regex REG_AHEAD = new Regex(@"ahead (\d+)");
private static readonly Regex REG_BEHIND = new Regex(@"behind (\d+)");
private List<Models.Branch> loaded = new List<Models.Branch>();
public Branches(string path) {
Cwd = path;
Args = CMD;
public QueryBranches(string repo) {
WorkingDirectory = repo;
Context = repo;
Args = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)\"";
}
public List<Models.Branch> Result() {
Exec();
return loaded;
return _branches;
}
public override void OnReadline(string line) {
protected override void OnReadline(string line) {
var parts = line.Split('$');
if (parts.Length != 5) return;
@ -55,7 +50,7 @@ namespace SourceGit.Commands {
branch.Upstream = parts[3];
branch.UpstreamTrackStatus = ParseTrackStatus(parts[4]);
loaded.Add(branch);
_branches.Add(branch);
}
private string ParseTrackStatus(string data) {
@ -75,5 +70,7 @@ namespace SourceGit.Commands {
return track.Trim();
}
private List<Models.Branch> _branches = new List<Models.Branch>();
}
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class QueryCommitChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
public QueryCommitChanges(string repo, string commitSHA) {
WorkingDirectory = repo;
Context = repo;
Args = $"show --name-status {commitSHA}";
}
public List<Models.Change> Result() {
Exec();
_changes.Sort((l, r) => l.Path.CompareTo(r.Path));
return _changes;
}
protected override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0]) {
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
case 'R': change.Set(Models.ChangeState.Renamed); _changes.Add(change); break;
case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); break;
}
}
private List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -1,13 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SourceGit.Commands {
/// <summary>
/// 取得提交列表
/// </summary>
public class Commits : Command {
public class QueryCommits : Command {
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
@ -17,8 +12,8 @@ namespace SourceGit.Commands {
private bool isHeadFounded = false;
private bool findFirstMerged = true;
public Commits(string path, string limits, bool needFindHead = true) {
Cwd = path;
public QueryCommits(string repo, string limits, bool needFindHead = true) {
WorkingDirectory = repo;
Args = "log --date-order --decorate=full --pretty=raw " + limits;
findFirstMerged = needFindHead;
}
@ -38,7 +33,7 @@ namespace SourceGit.Commands {
return commits;
}
public override void OnReadline(string line) {
protected override void OnReadline(string line) {
if (isSkipingGpgsig) {
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false;
return;
@ -137,10 +132,10 @@ namespace SourceGit.Commands {
}
private void MarkFirstMerged() {
Args = $"log --since=\"{commits.Last().CommitterTimeStr}\" --format=\"%H\"";
Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.Output.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0) return;
var set = new HashSet<string>();

View file

@ -1,26 +1,23 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands {
/// <summary>
/// 取得指定提交下的某文件内容
/// </summary>
public class QueryFileContent : Command {
private List<Models.TextLine> lines = new List<Models.TextLine>();
private int added = 0;
public QueryFileContent(string repo, string commit, string path) {
Cwd = repo;
Args = $"show {commit}:\"{path}\"";
public QueryFileContent(string repo, string revision, string file) {
WorkingDirectory = repo;
Context = repo;
Args = $"show {revision}:\"{file}\"";
}
public List<Models.TextLine> Result() {
public string Result() {
Exec();
return lines;
return _builder.ToString();
}
public override void OnReadline(string line) {
added++;
lines.Add(new Models.TextLine() { Number = added, Data = line });
protected override void OnReadline(string line) {
_builder.Append(line);
_builder.Append('\n');
}
private StringBuilder _builder = new StringBuilder();
}
}

View file

@ -0,0 +1,29 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class QueryFileSize : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$");
public QueryFileSize(string repo, string file, string revision) {
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree {revision} -l -- {file}";
}
public long Result() {
if (_result != 0) return _result;
var rs = ReadToEnd();
if (rs.IsSuccess) {
var match = REG_FORMAT.Match(rs.StdOut);
if (match.Success) {
return long.Parse(match.Groups[1].Value);
}
}
return 0;
}
private long _result = 0;
}
}

View file

@ -1,50 +0,0 @@
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 查询文件大小变化
/// </summary>
public class QueryFileSizeChange {
class QuerySizeCmd : Command {
public QuerySizeCmd(string repo, string path, string revision) {
Cwd = repo;
Args = $"cat-file -s {revision}:\"{path}\"";
}
public long Result() {
string data = ReadToEnd().Output;
long size;
if (!long.TryParse(data, out size)) size = 0;
return size;
}
}
private Models.FileSizeChange change = new Models.FileSizeChange();
public QueryFileSizeChange(string repo, string[] revisions, string path, string orgPath) {
if (revisions.Length == 0) {
change.NewSize = new FileInfo(Path.Combine(repo, path)).Length;
change.OldSize = new QuerySizeCmd(repo, path, "HEAD").Result();
} else if (revisions.Length == 1) {
change.NewSize = new QuerySizeCmd(repo, path, "HEAD").Result();
if (string.IsNullOrEmpty(orgPath)) {
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
} else {
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
}
} else {
change.NewSize = new QuerySizeCmd(repo, path, revisions[1]).Result();
if (string.IsNullOrEmpty(orgPath)) {
change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result();
} else {
change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result();
}
}
}
public Models.FileSizeChange Result() {
return change;
}
}
}

View file

@ -1,23 +1,20 @@
using System.IO;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 取得GitDir
/// </summary>
public class QueryGitDir : Command {
public QueryGitDir(string workDir) {
Cwd = workDir;
WorkingDirectory = workDir;
Args = "rev-parse --git-dir";
RaiseError = false;
}
public string Result() {
var rs = ReadToEnd().Output;
var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs)) return null;
rs = rs.Trim();
if (Path.IsPathRooted(rs)) return rs;
return Path.GetFullPath(Path.Combine(Cwd, rs));
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
}
}
}

View file

@ -1,28 +0,0 @@
using System;
namespace SourceGit.Commands {
/// <summary>
/// 取得一个LFS对象的信息
/// </summary>
public class QueryLFSObject : Command {
private Models.LFSObject obj = new Models.LFSObject();
public QueryLFSObject(string repo, string commit, string path) {
Cwd = repo;
Args = $"show {commit}:\"{path}\"";
}
public Models.LFSObject Result() {
Exec();
return obj;
}
public override void OnReadline(string line) {
if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) {
obj.OID = line.Substring(11).Trim();
} else if (line.StartsWith("size")) {
obj.Size = int.Parse(line.Substring(4).Trim());
}
}
}
}

View file

@ -1,41 +0,0 @@
namespace SourceGit.Commands {
/// <summary>
/// 查询LFS对象变更
/// </summary>
public class QueryLFSObjectChange : Command {
private Models.LFSChange change = new Models.LFSChange();
public QueryLFSObjectChange(string repo, string args) {
Cwd = repo;
Args = $"diff --ignore-cr-at-eol {args}";
}
public Models.LFSChange Result() {
Exec();
return change;
}
public override void OnReadline(string line) {
var ch = line[0];
if (ch == '-') {
if (change.Old == null) change.Old = new Models.LFSObject();
line = line.Substring(1);
if (line.StartsWith("oid sha256:")) {
change.Old.OID = line.Substring(11);
} else if (line.StartsWith("size ")) {
change.Old.Size = int.Parse(line.Substring(5));
}
} else if (ch == '+') {
if (change.New == null) change.New = new Models.LFSObject();
line = line.Substring(1);
if (line.StartsWith("oid sha256:")) {
change.New.OID = line.Substring(11);
} else if (line.StartsWith("size ")) {
change.New.Size = int.Parse(line.Substring(5));
}
} else if (line.StartsWith(" size ")) {
change.New.Size = change.Old.Size = int.Parse(line.Substring(6));
}
}
}
}

View file

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class QueryLocalChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private static readonly string[] UNTRACKED = [ "no", "all" ];
public QueryLocalChanges(string repo, bool includeUntracked = true) {
WorkingDirectory = repo;
Context = repo;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List<Models.Change> Result() {
Exec();
return _changes;
}
protected override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
if (line.EndsWith("/", StringComparison.Ordinal)) return; // Ignore changes with git-worktree
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status) {
case " M": change.Set(Models.ChangeState.None, Models.ChangeState.Modified); break;
case " A": change.Set(Models.ChangeState.None, Models.ChangeState.Added); break;
case " D": change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); break;
case " R": change.Set(Models.ChangeState.None, Models.ChangeState.Renamed); break;
case " C": change.Set(Models.ChangeState.None, Models.ChangeState.Copied); break;
case "M": change.Set(Models.ChangeState.Modified, Models.ChangeState.None); break;
case "MM": change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); break;
case "MD": change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); break;
case "A": change.Set(Models.ChangeState.Added, Models.ChangeState.None); break;
case "AM": change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); break;
case "AD": change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); break;
case "D": change.Set(Models.ChangeState.Deleted, Models.ChangeState.None); break;
case "R": change.Set(Models.ChangeState.Renamed, Models.ChangeState.None); break;
case "RM": change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); break;
case "RD": change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); break;
case "C": change.Set(Models.ChangeState.Copied, Models.ChangeState.None); break;
case "CM": change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); break;
case "CD": change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); break;
case "DR": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed); break;
case "DC": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied); break;
case "DD": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted); break;
case "AU": change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged); break;
case "UD": change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted); break;
case "UA": change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added); break;
case "DU": change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged); break;
case "AA": change.Set(Models.ChangeState.Added, Models.ChangeState.Added); break;
case "UU": change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged); break;
case "??": change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked); break;
default: return;
}
_changes.Add(change);
}
private List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -1,25 +1,22 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 获取远程列表
/// </summary>
public class Remotes : Command {
public class QueryRemotes : Command {
private static readonly Regex REG_REMOTE = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
private List<Models.Remote> loaded = new List<Models.Remote>();
public Remotes(string repo) {
Cwd = repo;
public QueryRemotes(string repo) {
WorkingDirectory = repo;
Context = repo;
Args = "remote -v";
}
public List<Models.Remote> Result() {
Exec();
return loaded;
return _loaded;
}
public override void OnReadline(string line) {
protected override void OnReadline(string line) {
var match = REG_REMOTE.Match(line);
if (!match.Success) return;
@ -28,8 +25,10 @@ namespace SourceGit.Commands {
URL = match.Groups[2].Value,
};
if (loaded.Find(x => x.Name == remote.Name) != null) return;
loaded.Add(remote);
if (_loaded.Find(x => x.Name == remote.Name) != null) return;
_loaded.Add(remote);
}
private List<Models.Remote> _loaded = new List<Models.Remote>();
}
}

View file

@ -0,0 +1,15 @@
namespace SourceGit.Commands {
public class QueryRepositoryRootPath : Command {
public QueryRepositoryRootPath(string path) {
WorkingDirectory = path;
Args = "rev-parse --show-toplevel";
RaiseError = false;
}
public string Result() {
var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs)) return null;
return rs.Trim();
}
}
}

View file

@ -1,16 +1,14 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 取出指定Revision下的文件列表
/// </summary>
public class RevisionObjects : Command {
public class QueryRevisionObjects : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$");
private List<Models.Object> objects = new List<Models.Object>();
public RevisionObjects(string cwd, string sha) {
Cwd = cwd;
public QueryRevisionObjects(string repo, string sha) {
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree -r {sha}";
}
@ -19,7 +17,7 @@ namespace SourceGit.Commands {
return objects;
}
public override void OnReadline(string line) {
protected override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class QueryStashChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
public QueryStashChanges(string repo, string sha) {
WorkingDirectory = repo;
Context = repo;
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
}
public List<Models.Change> Result() {
Exec();
return _changes;
}
protected override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0]) {
case 'M': change.Set(Models.ChangeState.Modified); _changes.Add(change); break;
case 'A': change.Set(Models.ChangeState.Added); _changes.Add(change); break;
case 'D': change.Set(Models.ChangeState.Deleted); _changes.Add(change); break;
case 'R': change.Set(Models.ChangeState.Renamed); _changes.Add(change); break;
case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); break;
}
}
private List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -1,48 +1,47 @@
using System;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 解析当前仓库中的贮藏
/// </summary>
public class Stashes : Command {
public class QueryStashes : Command {
private static readonly Regex REG_STASH = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$");
private List<Models.Stash> parsed = new List<Models.Stash>();
private Models.Stash current = null;
public Stashes(string path) {
Cwd = path;
public QueryStashes(string repo) {
WorkingDirectory = repo;
Context = repo;
Args = "stash list --pretty=raw";
}
public List<Models.Stash> Result() {
Exec();
if (current != null) parsed.Add(current);
return parsed;
if (_current != null) _stashes.Add(_current);
return _stashes;
}
public override void OnReadline(string line) {
protected override void OnReadline(string line) {
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
if (current != null && !string.IsNullOrEmpty(current.Name)) parsed.Add(current);
current = new Models.Stash() { SHA = line.Substring(7, 8) };
if (_current != null && !string.IsNullOrEmpty(_current.Name)) _stashes.Add(_current);
_current = new Models.Stash() { SHA = line.Substring(7, 8) };
return;
}
if (current == null) return;
if (_current == null) return;
if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) {
var match = REG_STASH.Match(line);
if (match.Success) current.Name = match.Groups[1].Value;
if (match.Success) _current.Name = match.Groups[1].Value;
} else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal)) {
current.Message = line.Substring(16);
_current.Message = line.Substring(16);
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
current.Author = user;
current.Time = time;
_current.Author = user;
_current.Time = time;
}
}
private List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
}
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
public class QuerySubmodules : Command {
private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$");
public QuerySubmodules(string repo) {
WorkingDirectory = repo;
Context = repo;
Args = "submodule status";
}
public List<string> Result() {
Exec();
return _submodules;
}
protected override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
_submodules.Add(match.Groups[1].Value);
}
private List<string> _submodules = new List<string>();
}
}

34
src/Commands/QueryTags.cs Normal file
View file

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands {
public class QueryTags : Command {
public QueryTags(string repo) {
Context = repo;
WorkingDirectory = repo;
Args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
}
public List<Models.Tag> Result() {
Exec();
return _loaded;
}
protected override void OnReadline(string line) {
var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 2) {
_loaded.Add(new Models.Tag() {
Name = subs[0],
SHA = subs[1],
});
} else if (subs.Length == 3) {
_loaded.Add(new Models.Tag() {
Name = subs[0],
SHA = subs[2],
});
}
}
private List<Models.Tag> _loaded = new List<Models.Tag>();
}
}

View file

@ -1,11 +1,8 @@
namespace SourceGit.Commands {
/// <summary>
/// 变基命令
/// </summary>
namespace SourceGit.Commands {
public class Rebase : Command {
public Rebase(string repo, string basedOn, bool autoStash) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = "rebase ";
if (autoStash) Args += "--autostash ";
Args += basedOn;

View file

@ -1,11 +1,8 @@
namespace SourceGit.Commands {
/// <summary>
/// 远程操作
/// </summary>
namespace SourceGit.Commands {
public class Remote : Command {
public Remote(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
}
public bool Add(string name, string url) {

View file

@ -1,33 +1,32 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands {
/// <summary>
/// 重置命令
/// </summary>
public class Reset : Command {
public Reset(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = "reset";
}
public Reset(string repo, string revision, string mode) {
Cwd = repo;
Args = $"reset {mode} {revision}";
}
public Reset(string repo, List<Models.Change> changes) {
WorkingDirectory = repo;
Context = repo;
public Reset(string repo, List<string> files) {
Cwd = repo;
StringBuilder builder = new StringBuilder();
var builder = new StringBuilder();
builder.Append("reset --");
foreach (var f in files) {
foreach (var c in changes) {
builder.Append(" \"");
builder.Append(f);
builder.Append(c.Path);
builder.Append("\"");
}
Args = builder.ToString();
}
public Reset(string repo, string revision, string mode) {
WorkingDirectory = repo;
Context = repo;
Args = $"reset {mode} {revision}";
}
}
}

View file

@ -1,11 +1,8 @@
namespace SourceGit.Commands {
/// <summary>
/// 撤销提交
/// </summary>
namespace SourceGit.Commands {
public class Revert : Command {
public Revert(string repo, string commit, bool autoCommit) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
Args = $"revert {commit} --no-edit";
if (!autoCommit) Args += " --no-commit";
}

View file

@ -1,16 +0,0 @@
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 编辑HEAD的提交信息
/// </summary>
public class Reword : Command {
public Reword(string repo, string msg) {
var tmp = Path.GetTempFileName();
File.WriteAllText(tmp, msg);
Cwd = repo;
Args = $"commit --amend --allow-empty --file=\"{tmp}\"";
}
}
}

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace SourceGit.Commands {
public static class SaveChangesAsPatch {
public static bool Exec(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo) {
using (var sw = File.Create(saveTo)) {
foreach (var change in changes) {
if (!ProcessSingleChange(repo, new Models.DiffOption(change, isUnstaged), sw)) return false;
}
}
return true;
}
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer) {
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitExecutableFile;
starter.Arguments = $"diff --ignore-cr-at-eol --unified=4 {opt}";
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardOutput = true;
try {
var proc = new Process() { StartInfo = starter };
proc.Start();
proc.StandardOutput.BaseStream.CopyTo(writer);
proc.WaitForExit();
var rs = proc.ExitCode == 0;
proc.Close();
return rs;
} catch (Exception e) {
App.RaiseException(repo, "Save change to patch failed: " + e.Message);
return false;
}
}
}
}

View file

@ -1,26 +0,0 @@
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 将Changes保存到文件流中
/// </summary>
public class SaveChangeToStream : Command {
private StreamWriter writer = null;
public SaveChangeToStream(string repo, Models.Change change, StreamWriter to) {
Cwd = repo;
if (change.WorkTree == Models.Change.Status.Added || change.WorkTree == Models.Change.Status.Untracked) {
Args = $"diff --no-index --no-ext-diff --find-renames -- /dev/null \"{change.Path}\"";
} else {
var pathspec = $"\"{change.Path}\"";
if (!string.IsNullOrEmpty(change.OriginalPath)) pathspec = $"\"{change.OriginalPath}\" \"{change.Path}\"";
Args = $"diff --binary --no-ext-diff --find-renames --full-index -- {pathspec}";
}
writer = to;
}
public override void OnReadline(string line) {
writer.WriteLine(line);
}
}
}

View file

@ -1,44 +1,60 @@
using System;
using System.Diagnostics;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 保存指定版本的文件
/// </summary>
public class SaveRevisionFile {
private string cwd = "";
private string bat = "";
public SaveRevisionFile(string repo, string path, string sha, string saveTo) {
var tmp = Path.GetTempFileName();
var cmd = $"\"{Models.Preference.Instance.Git.Path}\" --no-pager ";
var isLFS = new LFS(repo).IsFiltered(path);
if (isLFS) {
cmd += $"show {sha}:\"{path}\" > {tmp}.lfs\n";
cmd += $"\"{Models.Preference.Instance.Git.Path}\" --no-pager lfs smudge < {tmp}.lfs > \"{saveTo}\"\n";
public static class SaveRevisionFile {
public static void Run(string repo, string revision, string file, string saveTo) {
var isLFSFiltered = new IsLFSFiltered(repo, file).Result();
if (isLFSFiltered) {
var tmpFile = saveTo + ".tmp";
if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) {
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
}
File.Delete(tmpFile);
} else {
cmd += $"show {sha}:\"{path}\" > \"{saveTo}\"\n";
ExecCmd(repo, $"show {revision}:\"{file}\"", saveTo);
}
cwd = repo;
bat = tmp + ".bat";
File.WriteAllText(bat, cmd);
}
public void Exec() {
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) {
var starter = new ProcessStartInfo();
starter.FileName = bat;
starter.WorkingDirectory = cwd;
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitExecutableFile;
starter.Arguments = args;
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardInput = true;
starter.RedirectStandardOutput = true;
starter.RedirectStandardError = true;
var proc = Process.Start(starter);
proc.WaitForExit();
proc.Close();
using (var sw = File.OpenWrite(outputFile)) {
try {
var proc = new Process() { StartInfo = starter };
proc.Start();
File.Delete(bat);
if (inputFile != null) {
using (StreamReader sr = new StreamReader(inputFile)) {
while (true) {
var line = sr.ReadLine();
if (line == null) break;
proc.StandardInput.WriteLine(line);
}
}
}
proc.StandardOutput.BaseStream.CopyTo(sw);
proc.WaitForExit();
var rs = proc.ExitCode == 0;
proc.Close();
return rs;
} catch (Exception e) {
App.RaiseException(repo, "Save file failed: " + e.Message);
return false;
}
}
}
}
}

View file

@ -1,68 +1,49 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 单个贮藏相关操作
/// </summary>
public class Stash : Command {
public Stash(string repo) {
Cwd = repo;
WorkingDirectory = repo;
Context = repo;
}
public bool Push(List<Models.Change> changes, string message, bool bFull) {
if (bFull) {
var needAdd = new List<string>();
foreach (var c in changes) {
if (c.WorkTree == Models.Change.Status.Added || c.WorkTree == Models.Change.Status.Untracked) {
needAdd.Add(c.Path);
if (needAdd.Count > 10) {
new Add(Cwd, needAdd).Exec();
needAdd.Clear();
}
public bool Push(string message) {
Args = $"stash push -m \"{message}\"";
return Exec();
}
public bool Push(List<Models.Change> changes, string message) {
var temp = Path.GetTempFileName();
var stream = new FileStream(temp, FileMode.Create);
var writer = new StreamWriter(stream);
var needAdd = new List<Models.Change>();
foreach (var c in changes) {
writer.WriteLine(c.Path);
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) {
needAdd.Add(c);
if (needAdd.Count > 10) {
new Add(WorkingDirectory, needAdd).Exec();
needAdd.Clear();
}
}
}
if (needAdd.Count > 0) {
new Add(WorkingDirectory, needAdd).Exec();
needAdd.Clear();
}
if (needAdd.Count > 0) {
new Add(Cwd, needAdd).Exec();
needAdd.Clear();
}
writer.Flush();
stream.Flush();
writer.Close();
stream.Close();
Args = $"stash push -m \"{message}\"";
return Exec();
} else {
var temp = Path.GetTempFileName();
var stream = new FileStream(temp, FileMode.Create);
var writer = new StreamWriter(stream);
var needAdd = new List<string>();
foreach (var c in changes) {
writer.WriteLine(c.Path);
if (c.WorkTree == Models.Change.Status.Added || c.WorkTree == Models.Change.Status.Untracked) {
needAdd.Add(c.Path);
if (needAdd.Count > 10) {
new Add(Cwd, needAdd).Exec();
needAdd.Clear();
}
}
}
if (needAdd.Count > 0) {
new Add(Cwd, needAdd).Exec();
needAdd.Clear();
}
writer.Flush();
stream.Flush();
writer.Close();
stream.Close();
Args = $"stash push -m \"{message}\" --pathspec-from-file=\"{temp}\"";
var succ = Exec();
File.Delete(temp);
return succ;
}
Args = $"stash push -m \"{message}\" --pathspec-from-file=\"{temp}\"";
var succ = Exec();
File.Delete(temp);
return succ;
}
public bool Apply(string name) {
@ -79,5 +60,10 @@ namespace SourceGit.Commands {
Args = $"stash drop -q {name}";
return Exec();
}
public bool Clear() {
Args = "stash clear";
return Exec();
}
}
}

View file

@ -1,38 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 查看Stash中的修改
/// </summary>
public class StashChanges : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
private List<Models.Change> changes = new List<Models.Change>();
public StashChanges(string repo, string sha) {
Cwd = repo;
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
}
public List<Models.Change> Result() {
Exec();
return changes;
}
public override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0]) {
case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break;
case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break;
case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break;
case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break;
case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break;
}
}
}
}

View file

@ -1,43 +0,0 @@
using System;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 子树相关操作
/// </summary>
public class SubTree : Command {
private Action<string> handler = null;
public SubTree(string repo) {
Cwd = repo;
TraitErrorAsOutput = true;
}
public override void OnReadline(string line) {
handler?.Invoke(line);
}
public bool Add(string prefix, string source, string revision, bool squash, Action<string> onProgress) {
var path = Path.Combine(Cwd, prefix);
if (Directory.Exists(path)) return true;
handler = onProgress;
Args = $"subtree add --prefix=\"{prefix}\" {source} {revision}";
if (squash) Args += " --squash";
return Exec();
}
public void Pull(string prefix, string source, string branch, bool squash, Action<string> onProgress) {
handler = onProgress;
Args = $"subtree pull --prefix=\"{prefix}\" {source} {branch}";
if (squash) Args += " --squash";
Exec();
}
public void Push(string prefix, string source, string branch, Action<string> onProgress) {
handler = onProgress;
Args = $"subtree push --prefix=\"{prefix}\" {source} {branch}";
Exec();
}
}
}

View file

@ -1,25 +1,22 @@
using System;
using System;
namespace SourceGit.Commands {
/// <summary>
/// 子模块
/// </summary>
public class Submodule : Command {
private Action<string> onProgress = null;
public Submodule(string cwd) {
Cwd = cwd;
public Submodule(string repo) {
WorkingDirectory = repo;
Context = repo;
}
public bool Add(string url, string path, bool recursive, Action<string> handler) {
Args = $"submodule add {url} {path}";
onProgress = handler;
public bool Add(string url, string relativePath, bool recursive, Action<string> outputHandler) {
_outputHandler = outputHandler;
Args = $"submodule add {url} {relativePath}";
if (!Exec()) return false;
if (recursive) {
Args = $"submodule update --init --recursive -- {path}";
Args = $"submodule update --init --recursive -- {relativePath}";
return Exec();
} else {
Args = $"submodule update --init -- {relativePath}";
return true;
}
}
@ -29,16 +26,18 @@ namespace SourceGit.Commands {
return Exec();
}
public bool Delete(string path) {
Args = $"submodule deinit -f {path}";
public bool Delete(string relativePath) {
Args = $"submodule deinit -f {relativePath}";
if (!Exec()) return false;
Args = $"rm -rf {path}";
Args = $"rm -rf {relativePath}";
return Exec();
}
public override void OnReadline(string line) {
onProgress?.Invoke(line);
protected override void OnReadline(string line) {
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -1,28 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands {
/// <summary>
/// 获取子模块列表
/// </summary>
public class Submodules : Command {
private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$");
private List<string> modules = new List<string>();
public Submodules(string repo) {
Cwd = repo;
Args = "submodule status";
}
public List<string> Result() {
Exec();
return modules;
}
public override void OnReadline(string line) {
var match = REG_FORMAT.Match(line);
if (!match.Success) return;
modules.Add(match.Groups[1].Value);
}
}
}

View file

@ -1,38 +1,35 @@
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands {
/// <summary>
/// 标签相关指令
/// </summary>
public class Tag : Command {
public Tag(string repo) {
Cwd = repo;
}
public bool Add(string name, string basedOn, string message) {
Args = $"tag -a {name} {basedOn} ";
public static class Tag {
public static bool Add(string repo, string name, string basedOn, string message) {
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag -a {name} {basedOn} ";
if (!string.IsNullOrEmpty(message)) {
string tmp = Path.GetTempFileName();
File.WriteAllText(tmp, message);
Args += $"-F \"{tmp}\"";
cmd.Args += $"-F \"{tmp}\"";
} else {
Args += $"-m {name}";
cmd.Args += $"-m {name}";
}
return Exec();
return cmd.Exec();
}
public bool Delete(string name, bool push) {
Args = $"tag --delete {name}";
if (!Exec()) return false;
public static bool Delete(string repo, string name, List<Models.Remote> remotes) {
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag --delete {name}";
if (!cmd.Exec()) return false;
if (push) {
var remotes = new Remotes(Cwd).Result();
if (remotes != null) {
foreach (var r in remotes) {
new Push(Cwd, r.Name, name, true).Exec();
new Push(repo, r.Name, name, true).Exec();
}
}

View file

@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands {
/// <summary>
/// 解析所有的Tags
/// </summary>
public class Tags : Command {
public static readonly string CMD = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
private List<Models.Tag> loaded = new List<Models.Tag>();
public Tags(string path) {
Cwd = path;
Args = CMD;
}
public List<Models.Tag> Result() {
Exec();
return loaded;
}
public override void OnReadline(string line) {
var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 2) {
loaded.Add(new Models.Tag() {
Name = subs[0],
SHA = subs[1],
});
} else if (subs.Length == 3) {
loaded.Add(new Models.Tag() {
Name = subs[0],
SHA = subs[2],
});
}
}
}
}

View file

@ -1,18 +1,14 @@
using System;
namespace SourceGit.Commands {
/// <summary>
/// 检测git是否可用并获取git版本信息
/// </summary>
namespace SourceGit.Commands {
public class Version : Command {
const string GitVersionPrefix = "git version ";
public Version() {
Args = "-v";
RaiseError = false;
}
public string Query() {
Args = $"--version";
var result = ReadToEnd();
if (!result.IsSuccess || string.IsNullOrEmpty(result.Output)) return null;
var version = result.Output.Trim();
if (!version.StartsWith(GitVersionPrefix, StringComparison.Ordinal)) return null;
return version.Substring(GitVersionPrefix.Length);
var rs = ReadToEnd();
if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) return string.Empty;
return rs.StdOut.Trim().Substring("git version ".Length);
}
}
}