project: reorganize the structure of the project.

* remove dotnet-tool.json because the project does not rely on any dotnet tools.
* remove Directory.Build.props because the solution has only one project.
* move src/SourceGit to src. It's not needed to put all sources into a subfolder of src since there's only one project.
This commit is contained in:
leo 2024-04-02 20:00:33 +08:00
parent 96e60da7ad
commit 96d4150d26
319 changed files with 37 additions and 53 deletions

31
src/Commands/Add.cs Normal file
View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Add : Command
{
public Add(string repo, List<Models.Change> changes = null)
{
WorkingDirectory = repo;
Context = repo;
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();
}
}
}
}

19
src/Commands/Apply.cs Normal file
View file

@ -0,0 +1,19 @@
namespace SourceGit.Commands
{
public class Apply : Command
{
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra)
{
WorkingDirectory = repo;
Context = repo;
Args = "apply ";
if (ignoreWhitespace)
Args += "--ignore-whitespace ";
else
Args += $"--whitespace={whitespaceMode} ";
if (!string.IsNullOrEmpty(extra))
Args += $"{extra} ";
Args += $"\"{file}\"";
}
}
}

23
src/Commands/Archive.cs Normal file
View file

@ -0,0 +1,23 @@
using System;
namespace SourceGit.Commands
{
public class Archive : Command
{
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;
_outputHandler = outputHandler;
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

View file

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class AssumeUnchanged
{
partial class ViewCommand : Command
{
[GeneratedRegex(@"^(\w)\s+(.+)$")]
private static partial Regex REG();
public ViewCommand(string repo)
{
WorkingDirectory = repo;
Args = "ls-files -v";
RaiseError = false;
}
public List<string> Result()
{
Exec();
return _outs;
}
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);
}
}
private readonly 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";
WorkingDirectory = repo;
Context = repo;
Args = $"update-index {mode} -- \"{file}\"";
}
}
public AssumeUnchanged(string repo)
{
_repo = repo;
}
public List<string> View()
{
return new ViewCommand(_repo).Result();
}
public void Add(string file)
{
new ModCommand(_repo, file, true).Exec();
}
public void Remove(string file)
{
new ModCommand(_repo, file, false).Exec();
}
private readonly string _repo;
}
}

96
src/Commands/Blame.cs Normal file
View file

@ -0,0 +1,96 @@
using System;
using System.Text;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class Blame : Command
{
[GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")]
private static partial Regex REG_FORMAT();
private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
public Blame(string repo, string file, string revision)
{
WorkingDirectory = repo;
Context = repo;
Args = $"blame -t {revision} -- \"{file}\"";
RaiseError = false;
_result.File = file;
}
public Models.BlameData Result()
{
var succ = Exec();
if (!succ)
{
return new Models.BlameData();
}
if (_needUnifyCommitSHA)
{
foreach (var line in _result.LineInfos)
{
if (line.CommitSHA.Length > _minSHALen)
{
line.CommitSHA = line.CommitSHA.Substring(0, _minSHALen);
}
}
}
_result.Content = _content.ToString();
return _result;
}
protected override void OnReadline(string line)
{
if (_result.IsBinary)
return;
if (string.IsNullOrEmpty(line))
return;
if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
{
_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 when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd");
var info = new Models.BlameLineInfo()
{
IsFirstInGroup = commit != _lastSHA,
CommitSHA = commit,
Author = author,
Time = when,
};
_result.LineInfos.Add(info);
_lastSHA = commit;
if (line[0] == '^')
{
_needUnifyCommitSHA = true;
_minSHALen = Math.Min(_minSHALen, commit.Length);
}
}
private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder();
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;
}
}

48
src/Commands/Branch.cs Normal file
View file

@ -0,0 +1,48 @@
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 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 static bool SetUpstream(string repo, string name, string upstream)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
if (string.IsNullOrEmpty(upstream))
{
cmd.Args = $"branch {name} --unset-upstream";
}
else
{
cmd.Args = $"branch {name} -u {upstream}";
}
return cmd.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();
}
}
}

72
src/Commands/Checkout.cs Normal file
View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Checkout : Command
{
public Checkout(string repo)
{
WorkingDirectory = repo;
Context = repo;
}
public bool Branch(string branch, Action<string> onProgress)
{
Args = $"checkout --progress {branch}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
return Exec();
}
public bool Branch(string branch, string basedOn, Action<string> onProgress)
{
Args = $"checkout --progress -b {branch} {basedOn}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
return Exec();
}
public bool File(string file, bool useTheirs)
{
if (useTheirs)
{
Args = $"checkout --theirs -- \"{file}\"";
}
else
{
Args = $"checkout --ours -- \"{file}\"";
}
return Exec();
}
public bool FileWithRevision(string file, string revision)
{
Args = $"checkout {revision} -- \"{file}\"";
return Exec();
}
public bool Files(List<string> files)
{
StringBuilder builder = new StringBuilder();
builder.Append("checkout -f -q --");
foreach (var f in files)
{
builder.Append(" \"");
builder.Append(f);
builder.Append("\"");
}
Args = builder.ToString();
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -0,0 +1,13 @@
namespace SourceGit.Commands
{
public class CherryPick : Command
{
public CherryPick(string repo, string commit, bool noCommit)
{
var mode = noCommit ? "-n" : "--ff";
WorkingDirectory = repo;
Context = repo;
Args = $"cherry-pick {mode} {commit}";
}
}
}

31
src/Commands/Clean.cs Normal file
View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Clean : Command
{
public Clean(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "clean -qfd";
}
public Clean(string repo, List<string> files)
{
StringBuilder builder = new StringBuilder();
builder.Append("clean -qfd --");
foreach (var f in files)
{
builder.Append(" \"");
builder.Append(f);
builder.Append("\"");
}
WorkingDirectory = repo;
Context = repo;
Args = builder.ToString();
}
}
}

40
src/Commands/Clone.cs Normal file
View file

@ -0,0 +1,40 @@
using System;
namespace SourceGit.Commands
{
public class Clone : Command
{
private readonly Action<string> _notifyProgress;
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler)
{
Context = ctx;
WorkingDirectory = path;
TraitErrorAsOutput = true;
if (string.IsNullOrEmpty(sshKey))
{
Args = "-c credential.helper=manager ";
}
else
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
Args += "clone --progress --verbose --recurse-submodules ";
if (!string.IsNullOrEmpty(extraArgs))
Args += $"{extraArgs} ";
Args += $"{url} ";
if (!string.IsNullOrEmpty(localName))
Args += localName;
_notifyProgress = ouputHandler;
}
protected override void OnReadline(string line)
{
_notifyProgress?.Invoke(line);
}
}
}

192
src/Commands/Command.cs Normal file
View file

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public partial class Command
{
public class CancelToken
{
public bool Requested { get; set; } = false;
}
public class ReadToEndResult
{
public bool IsSuccess { get; set; }
public string StdOut { get; set; }
public string StdErr { get; set; }
}
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;
public bool Exec()
{
var start = new ProcessStartInfo();
start.FileName = Native.OS.GitInstallPath;
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
{
start.Environment.Add("LANG", "en_US.UTF-8");
}
if (!string.IsNullOrEmpty(WorkingDirectory))
start.WorkingDirectory = WorkingDirectory;
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
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 (_progressRegex().IsMatch(e.Data))
return;
errs.Add(e.Data);
};
try
{
proc.Start();
}
catch (Exception e)
{
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, e.Message);
});
}
return false;
}
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
proc.WaitForExit();
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0 && errs.Count > 0)
{
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, string.Join("\n", errs));
});
}
return false;
}
else
{
return true;
}
}
public ReadToEndResult ReadToEnd()
{
var start = new ProcessStartInfo();
start.FileName = Native.OS.GitInstallPath;
start.Arguments = "--no-pager -c core.quotepath=off " + Args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
if (!string.IsNullOrEmpty(WorkingDirectory))
start.WorkingDirectory = WorkingDirectory;
var proc = new Process() { StartInfo = start };
try
{
proc.Start();
}
catch (Exception e)
{
return new ReadToEndResult()
{
IsSuccess = false,
StdOut = string.Empty,
StdErr = e.Message,
};
}
var rs = new ReadToEndResult()
{
StdOut = proc.StandardOutput.ReadToEnd(),
StdErr = proc.StandardError.ReadToEnd(),
};
proc.WaitForExit();
rs.IsSuccess = proc.ExitCode == 0;
proc.Close();
return rs;
}
protected virtual void OnReadline(string line) { }
[GeneratedRegex(@"\d+%")]
private static partial Regex _progressRegex();
}
}

21
src/Commands/Commit.cs Normal file
View file

@ -0,0 +1,21 @@
using System.IO;
namespace SourceGit.Commands
{
public class Commit : Command
{
public Commit(string repo, string message, bool amend, bool allowEmpty = false)
{
var file = Path.GetTempFileName();
File.WriteAllText(file, message);
WorkingDirectory = repo;
Context = repo;
Args = $"commit --file=\"{file}\"";
if (amend)
Args += " --amend --no-edit";
if (allowEmpty)
Args += " --allow-empty";
}
}
}

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class CompareRevisions : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
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 readonly List<Models.Change> _changes = new List<Models.Change>();
}
}

80
src/Commands/Config.cs Normal file
View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class Config : Command
{
public Config(string repository)
{
WorkingDirectory = repository;
Context = repository;
RaiseError = false;
}
public Dictionary<string, string> ListAll()
{
Args = "config -l";
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('=', StringComparison.Ordinal);
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().StdOut.Trim();
}
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.IsNullOrWhiteSpace(WorkingDirectory))
{
Args = $"config --global {key} \"{value}\"";
}
else
{
Args = $"config {key} \"{value}\"";
}
}
return Exec();
}
}
}

211
src/Commands/Diff.cs Normal file
View file

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class Diff : Command
{
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR();
private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/";
private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/";
private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/";
public Diff(string repo, Models.DiffOption opt)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff --ignore-cr-at-eol --unified=4 {opt}";
}
public Models.DiffResult Result()
{
Exec();
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;
}
protected override void OnReadline(string line)
{
if (_result.IsBinary)
return;
if (_result.IsLFS)
{
var ch = line[0];
if (ch == '-')
{
if (line.StartsWith("-oid sha256:", StringComparison.Ordinal))
{
_result.LFSDiff.Old.Oid = line.Substring(12);
}
else if (line.StartsWith("-size ", StringComparison.Ordinal))
{
_result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
}
}
else if (ch == '+')
{
if (line.StartsWith("+oid sha256:", StringComparison.Ordinal))
{
_result.LFSDiff.New.Oid = line.Substring(12);
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = long.Parse(line.Substring(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
_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))
_result.IsBinary = true;
return;
}
_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, 0, 0));
}
else
{
if (line.Length == 0)
{
ProcessInlineHighlights();
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
_oldLine++;
_newLine++;
return;
}
var ch = line[0];
if (ch == '-')
{
if (_oldLine == 1 && _newLine == 0 && line.StartsWith(PREFIX_LFS_DEL, StringComparison.Ordinal))
{
_result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff();
return;
}
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
_oldLine++;
}
else if (ch == '+')
{
if (_oldLine == 0 && _newLine == 1 && line.StartsWith(PREFIX_LFS_NEW, StringComparison.Ordinal))
{
_result.IsLFS = true;
_result.LFSDiff = new Models.LFSDiff();
return;
}
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
_newLine++;
}
else if (ch != '\\')
{
ProcessInlineHighlights();
var match = REG_INDICATOR().Match(line);
if (match.Success)
{
_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, 0, 0));
}
else
{
if (_oldLine == 1 && _newLine == 1 && line.StartsWith(PREFIX_LFS_MODIFY, StringComparison.Ordinal))
{
_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 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.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.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount));
}
if (chunk.AddedCount > 0)
{
right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount));
}
}
}
}
_result.TextDiff.Lines.AddRange(_deleted);
_deleted.Clear();
}
if (_added.Count > 0)
{
_result.TextDiff.Lines.AddRange(_added);
_added.Clear();
}
}
private readonly Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() };
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private int _oldLine = 0;
private int _newLine = 0;
}
}

56
src/Commands/Discard.cs Normal file
View file

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public static class Discard
{
public static void All(string repo)
{
new Reset(repo, "HEAD", "--hard").Exec();
new Clean(repo).Exec();
}
public static void ChangesInWorkTree(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.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
{
needClean.Add(c.Path);
}
else
{
needCheckout.Add(c.Path);
}
}
for (int i = 0; i < needClean.Count; i += 10)
{
var count = Math.Min(10, needClean.Count - i);
new Clean(repo, needClean.GetRange(i, count)).Exec();
}
for (int i = 0; i < needCheckout.Count; i += 10)
{
var count = Math.Min(10, needCheckout.Count - i);
new Checkout(repo).Files(needCheckout.GetRange(i, count));
}
}
public static void ChangesInStaged(string repo, List<Models.Change> changes)
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var files = new List<string>();
for (int j = 0; j < count; j++)
files.Add(changes[i + j].Path);
new Restore(repo, files, "--staged --worktree").Exec();
}
}
}
}

158
src/Commands/Fetch.cs Normal file
View file

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace SourceGit.Commands
{
public class Fetch : Command
{
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
Args = "-c credential.helper=manager ";
}
Args += "fetch --progress --verbose ";
if (prune)
Args += "--prune ";
Args += remote;
AutoFetch.MarkFetched(repo);
}
public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
Args = "-c credential.helper=manager ";
}
Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
public class AutoFetch
{
private const double INTERVAL = 10 * 60;
public static bool IsEnabled
{
get;
set;
} = false;
class Job
{
public Fetch Cmd = null;
public DateTime NextRunTimepoint = DateTime.MinValue;
}
static AutoFetch()
{
Task.Run(() =>
{
while (true)
{
if (!IsEnabled)
{
Thread.Sleep(10000);
continue;
}
var now = DateTime.Now;
var uptodate = new List<Job>();
lock (_lock)
{
foreach (var job in _jobs)
{
if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0)
{
uptodate.Add(job.Value);
}
}
}
foreach (var job in uptodate)
{
job.Cmd.Exec();
job.NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL);
}
Thread.Sleep(2000);
}
});
}
public static void AddRepository(string repo)
{
var job = new Job
{
Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false },
NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL),
};
lock (_lock)
{
if (_jobs.ContainsKey(repo))
{
_jobs[repo] = job;
}
else
{
_jobs.Add(repo, job);
}
}
}
public static void RemoveRepository(string repo)
{
lock (_lock)
{
_jobs.Remove(repo);
}
}
public static void MarkFetched(string repo)
{
lock (_lock)
{
if (_jobs.TryGetValue(repo, out var value))
{
value.NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL);
}
}
}
private static readonly Dictionary<string, Job> _jobs = new Dictionary<string, Job>();
private static readonly object _lock = new object();
}
}

View file

@ -0,0 +1,12 @@
namespace SourceGit.Commands
{
public class FormatPatch : Command
{
public FormatPatch(string repo, string commit, string saveTo)
{
WorkingDirectory = repo;
Context = repo;
Args = $"format-patch {commit} -1 -o \"{saveTo}\"";
}
}
}

23
src/Commands/GC.cs Normal file
View file

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

View file

@ -0,0 +1,40 @@
using System;
using System.Diagnostics;
using System.IO;
using Avalonia.Media.Imaging;
namespace SourceGit.Commands
{
public static class GetImageFileAsBitmap
{
public static Bitmap Run(string repo, string revision, string file)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitInstallPath;
starter.Arguments = $"show {revision}:\"{file}\"";
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardOutput = true;
try
{
var stream = new MemoryStream();
var proc = new Process() { StartInfo = starter };
proc.Start();
proc.StandardOutput.BaseStream.CopyTo(stream);
proc.WaitForExit();
proc.Close();
stream.Position = 0;
return new Bitmap(stream);
}
catch
{
return null;
}
}
}
}

90
src/Commands/GitFlow.cs Normal file
View file

@ -0,0 +1,90 @@
using System.Collections.Generic;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public class GitFlow : Command
{
public GitFlow(string repo)
{
WorkingDirectory = repo;
Context = repo;
}
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)
Branch.Create(WorkingDirectory, master, current.Head);
var devBranch = branches.Find(x => x.Name == develop);
if (devBranch == null && current != null)
Branch.Create(WorkingDirectory, develop, current.Head);
var cmd = new Config(WorkingDirectory);
cmd.Set("gitflow.branch.master", master);
cmd.Set("gitflow.branch.develop", develop);
cmd.Set("gitflow.prefix.feature", feature);
cmd.Set("gitflow.prefix.bugfix", "bugfix/");
cmd.Set("gitflow.prefix.release", release);
cmd.Set("gitflow.prefix.hotfix", hotfix);
cmd.Set("gitflow.prefix.support", "support/");
cmd.Set("gitflow.prefix.versiontag", version, true);
Args = "flow init -d";
return Exec();
}
public bool Start(Models.GitFlowBranchType type, string name)
{
switch (type)
{
case Models.GitFlowBranchType.Feature:
Args = $"flow feature start {name}";
break;
case Models.GitFlowBranchType.Release:
Args = $"flow release start {name}";
break;
case Models.GitFlowBranchType.Hotfix:
Args = $"flow hotfix start {name}";
break;
default:
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, "Bad branch type!!!");
});
return false;
}
return Exec();
}
public bool Finish(Models.GitFlowBranchType type, string name, bool keepBranch)
{
var option = keepBranch ? "-k" : string.Empty;
switch (type)
{
case Models.GitFlowBranchType.Feature:
Args = $"flow feature finish {option} {name}";
break;
case Models.GitFlowBranchType.Release:
Args = $"flow release finish {option} {name} -m \"RELEASE_DONE\"";
break;
case Models.GitFlowBranchType.Hotfix:
Args = $"flow hotfix finish {option} {name} -m \"HOTFIX_DONE\"";
break;
default:
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, "Bad branch type!!!");
});
return false;
}
return Exec();
}
}
}

12
src/Commands/Init.cs Normal file
View file

@ -0,0 +1,12 @@
namespace SourceGit.Commands
{
public class Init : Command
{
public Init(string ctx, string dir)
{
Context = ctx;
WorkingDirectory = dir;
Args = "init -q";
}
}
}

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

@ -0,0 +1,23 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class IsBinary : Command
{
[GeneratedRegex(@"^\-\s+\-\s+.*$")]
private static partial Regex REG_TEST();
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

@ -0,0 +1,19 @@
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");
}
}
}

49
src/Commands/LFS.cs Normal file
View file

@ -0,0 +1,49 @@
using System;
using System.IO;
namespace SourceGit.Commands
{
public class LFS
{
class PruneCmd : Command
{
public PruneCmd(string repo, Action<string> onProgress)
{
WorkingDirectory = repo;
Context = repo;
Args = "lfs prune";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
public LFS(string repo)
{
_repo = repo;
}
public bool IsEnabled()
{
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 void Prune(Action<string> outputHandler)
{
new PruneCmd(_repo, outputHandler).Exec();
}
private readonly string _repo;
}
}

23
src/Commands/Merge.cs Normal file
View file

@ -0,0 +1,23 @@
using System;
namespace SourceGit.Commands
{
public class Merge : Command
{
public Merge(string repo, string source, string mode, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
Args = $"merge --progress {source} {mode}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
}
}

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

@ -0,0 +1,63 @@
using System.IO;
using Avalonia.Threading;
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))
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Invalid external merge tool settings!");
});
return false;
}
if (!File.Exists(tool))
{
Dispatcher.UIThread.Invoke(() =>
{
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))
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Invalid external merge tool settings!");
});
return false;
}
if (!File.Exists(tool))
{
Dispatcher.UIThread.Invoke(() =>
{
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();
}
}
}

37
src/Commands/Pull.cs Normal file
View file

@ -0,0 +1,37 @@
using System;
namespace SourceGit.Commands
{
public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
Args = "-c credential.helper=manager ";
}
Args += "pull --verbose --progress --tags ";
if (useRebase)
Args += "--rebase ";
Args += $"{remote} {branch}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

89
src/Commands/Push.cs Normal file
View file

@ -0,0 +1,89 @@
using System;
namespace SourceGit.Commands
{
public class Push : Command
{
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action<string> onProgress)
{
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
_outputHandler = onProgress;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
Args = "-c credential.helper=manager ";
}
Args += "push --progress --verbose ";
if (withTags)
Args += "--tags ";
if (track)
Args += "-u ";
if (force)
Args += "--force-with-lease ";
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)
{
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
Args = "-c credential.helper=manager ";
}
Args += $"push {remote} --delete {branch}";
}
public Push(string repo, string remote, string tag, bool isDelete)
{
WorkingDirectory = repo;
Context = repo;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
Args = "-c credential.helper=manager ";
}
Args += "push ";
if (isDelete)
Args += "--delete ";
Args += $"{remote} refs/tags/{tag}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
}
}

View file

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryBranches : Command
{
private const string PREFIX_LOCAL = "refs/heads/";
private const string PREFIX_REMOTE = "refs/remotes/";
[GeneratedRegex(@"^(\d+)\s(\d+)$")]
private static partial Regex REG_AHEAD_BEHIND();
public QueryBranches(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "branch -l --all -v --format=\"%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:trackshort)\"";
}
public List<Models.Branch> Result()
{
Exec();
foreach (var b in _branches)
{
if (b.IsLocal && !string.IsNullOrEmpty(b.UpstreamTrackStatus))
{
if (b.UpstreamTrackStatus == "=")
{
b.UpstreamTrackStatus = string.Empty;
}
else
{
b.UpstreamTrackStatus = ParseTrackStatus(b.Name, b.Upstream);
}
}
}
return _branches;
}
protected override void OnReadline(string line)
{
var parts = line.Split('$');
if (parts.Length != 5)
return;
var branch = new Models.Branch();
var refName = parts[0];
if (refName.EndsWith("/HEAD", StringComparison.Ordinal))
return;
if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal))
{
branch.Name = refName.Substring(PREFIX_LOCAL.Length);
branch.IsLocal = true;
}
else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal))
{
var name = refName.Substring(PREFIX_REMOTE.Length);
var shortNameIdx = name.IndexOf('/', StringComparison.Ordinal);
if (shortNameIdx < 0)
return;
branch.Remote = name.Substring(0, shortNameIdx);
branch.Name = name.Substring(branch.Remote.Length + 1);
branch.IsLocal = false;
}
else
{
branch.Name = refName;
branch.IsLocal = true;
}
branch.FullName = refName;
branch.Head = parts[1];
branch.IsCurrent = parts[2] == "*";
branch.Upstream = parts[3];
branch.UpstreamTrackStatus = parts[4];
_branches.Add(branch);
}
private string ParseTrackStatus(string local, string upstream)
{
var cmd = new Command();
cmd.WorkingDirectory = WorkingDirectory;
cmd.Context = Context;
cmd.Args = $"rev-list --left-right --count {local}...{upstream}";
var rs = cmd.ReadToEnd();
if (!rs.IsSuccess)
return string.Empty;
var match = REG_AHEAD_BEHIND().Match(rs.StdOut);
if (!match.Success)
return string.Empty;
var ahead = int.Parse(match.Groups[1].Value);
var behind = int.Parse(match.Groups[2].Value);
var track = "";
if (ahead > 0)
track += $"{ahead}↑";
if (behind > 0)
track += $" {behind}↓";
return track.Trim();
}
private readonly List<Models.Branch> _branches = new List<Models.Branch>();
}
}

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryCommitChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
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 readonly List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryCommits : Command
{
private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private const string GPGSIG_END = " -----END PGP SIGNATURE-----";
private readonly List<Models.Commit> commits = new List<Models.Commit>();
private Models.Commit current = null;
private bool isSkipingGpgsig = false;
private bool isHeadFounded = false;
private readonly bool findFirstMerged = true;
public QueryCommits(string repo, string limits, bool needFindHead = true)
{
WorkingDirectory = repo;
Args = "log --date-order --decorate=full --pretty=raw " + limits;
findFirstMerged = needFindHead;
}
public List<Models.Commit> Result()
{
Exec();
if (current != null)
{
current.Message = current.Message.Trim();
commits.Add(current);
}
if (findFirstMerged && !isHeadFounded && commits.Count > 0)
{
MarkFirstMerged();
}
return commits;
}
protected override void OnReadline(string line)
{
if (isSkipingGpgsig)
{
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal))
isSkipingGpgsig = false;
return;
}
else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal))
{
isSkipingGpgsig = true;
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
if (current != null)
{
current.Message = current.Message.Trim();
commits.Add(current);
}
current = new Models.Commit();
line = line.Substring(7);
var decoratorStart = line.IndexOf('(', StringComparison.Ordinal);
if (decoratorStart < 0)
{
current.SHA = line.Trim();
}
else
{
current.SHA = line.Substring(0, decoratorStart).Trim();
current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1));
if (!isHeadFounded)
isHeadFounded = current.IsMerged;
}
return;
}
if (current == null)
return;
if (line.StartsWith("tree ", StringComparison.Ordinal))
{
return;
}
else if (line.StartsWith("parent ", StringComparison.Ordinal))
{
current.Parents.Add(line.Substring("parent ".Length));
}
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
current.Author = user;
current.AuthorTime = time;
}
else if (line.StartsWith("committer ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time);
current.Committer = user;
current.CommitterTime = time;
}
else if (string.IsNullOrEmpty(current.Subject))
{
current.Subject = line.Trim();
}
else
{
current.Message += (line.Trim() + "\n");
}
}
private bool ParseDecorators(List<Models.Decorator> decorators, string data)
{
bool isHeadOfCurrent = false;
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs)
{
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.Tag,
Name = d.Substring(15).Trim(),
});
}
else if (d.EndsWith("/HEAD", StringComparison.Ordinal))
{
continue;
}
else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal))
{
isHeadOfCurrent = true;
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim(),
});
}
else if (d.StartsWith("refs/heads/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim(),
});
}
else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim(),
});
}
}
decorators.Sort((l, r) =>
{
if (l.Type != r.Type)
{
return (int)l.Type - (int)r.Type;
}
else
{
return l.Name.CompareTo(r.Name);
}
});
return isHeadOfCurrent;
}
private void MarkFirstMerged()
{
Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0)
return;
var set = new HashSet<string>();
foreach (var sha in shas)
set.Add(sha);
foreach (var c in commits)
{
if (set.Contains(c.SHA))
{
c.IsMerged = true;
break;
}
}
}
}
}

View file

@ -0,0 +1,28 @@
using System.Text;
namespace SourceGit.Commands
{
public class QueryFileContent : Command
{
public QueryFileContent(string repo, string revision, string file)
{
WorkingDirectory = repo;
Context = repo;
Args = $"show {revision}:\"{file}\"";
}
public string Result()
{
Exec();
return _builder.ToString();
}
protected override void OnReadline(string line)
{
_builder.Append(line);
_builder.Append('\n');
}
private readonly StringBuilder _builder = new StringBuilder();
}
}

View file

@ -0,0 +1,38 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryFileSize : Command
{
[GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")]
private static partial Regex REG_FORMAT();
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 readonly long _result = 0;
}
}

View file

@ -0,0 +1,26 @@
using System.IO;
namespace SourceGit.Commands
{
public class QueryGitDir : Command
{
public QueryGitDir(string workDir)
{
WorkingDirectory = workDir;
Args = "rev-parse --git-dir";
RaiseError = false;
}
public string Result()
{
var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs))
return null;
rs = rs.Trim();
if (Path.IsPathRooted(rs))
return rs;
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
}
}
}

View file

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryLocalChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
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 readonly List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryRemotes : Command
{
[GeneratedRegex(@"^([\w\.\-]+)\s*(\S+).*$")]
private static partial Regex REG_REMOTE();
public QueryRemotes(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "remote -v";
}
public List<Models.Remote> Result()
{
Exec();
return _loaded;
}
protected override void OnReadline(string line)
{
var match = REG_REMOTE().Match(line);
if (!match.Success)
return;
var remote = new Models.Remote()
{
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (_loaded.Find(x => x.Name == remote.Name) != null)
return;
_loaded.Add(remote);
}
private readonly List<Models.Remote> _loaded = new List<Models.Remote>();
}
}

View file

@ -0,0 +1,20 @@
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

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryRevisionObjects : Command
{
[GeneratedRegex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$")]
private static partial Regex REG_FORMAT();
private readonly List<Models.Object> objects = new List<Models.Object>();
public QueryRevisionObjects(string repo, string sha)
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree -r {sha}";
}
public List<Models.Object> Result()
{
Exec();
return objects;
}
protected override void OnReadline(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
return;
var obj = new Models.Object();
obj.SHA = match.Groups[2].Value;
obj.Type = Models.ObjectType.Blob;
obj.Path = match.Groups[3].Value;
switch (match.Groups[1].Value)
{
case "blob":
obj.Type = Models.ObjectType.Blob;
break;
case "tree":
obj.Type = Models.ObjectType.Tree;
break;
case "tag":
obj.Type = Models.ObjectType.Tag;
break;
case "commit":
obj.Type = Models.ObjectType.Commit;
break;
}
objects.Add(obj);
}
}
}

View file

@ -0,0 +1,29 @@
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryStagedFileBlobGuid : Command
{
[GeneratedRegex(@"^\d+\s+([0-9a-f]+)\s+.*$")]
private static partial Regex REG_FORMAT();
public QueryStagedFileBlobGuid(string repo, string file)
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-files -s -- \"{file}\"";
}
public string Result()
{
var rs = ReadToEnd();
var match = REG_FORMAT().Match(rs.StdOut.Trim());
if (match.Success)
{
return match.Groups[1].Value;
}
return string.Empty;
}
}
}

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryStashChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();
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 readonly List<Models.Change> _changes = new List<Models.Change>();
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryStashes : Command
{
[GeneratedRegex(@"^Reflog: refs/(stash@\{\d+\}).*$")]
private static partial Regex REG_STASH();
public QueryStashes(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "stash list --pretty=raw";
}
public List<Models.Stash> Result()
{
Exec();
if (_current != null)
_stashes.Add(_current);
return _stashes;
}
protected override void OnReadline(string line)
{
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
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 (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal))
{
var match = REG_STASH().Match(line);
if (match.Success)
_current.Name = match.Groups[1].Value;
}
else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal))
{
_current.Message = line.Substring(16);
}
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
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;
}
}
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
}
}

View file

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QuerySubmodules : Command
{
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
private static partial Regex REG_FORMAT2();
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_FORMAT1().Match(line);
if (match.Success)
{
_submodules.Add(match.Groups[1].Value);
return;
}
match = REG_FORMAT2().Match(line);
if (match.Success)
{
_submodules.Add(match.Groups[1].Value);
}
}
private readonly List<string> _submodules = new List<string>();
}
}

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

@ -0,0 +1,44 @@
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 readonly List<Models.Tag> _loaded = new List<Models.Tag>();
}
}

15
src/Commands/Rebase.cs Normal file
View file

@ -0,0 +1,15 @@
namespace SourceGit.Commands
{
public class Rebase : Command
{
public Rebase(string repo, string basedOn, bool autoStash)
{
WorkingDirectory = repo;
Context = repo;
Args = "rebase ";
if (autoStash)
Args += "--autostash ";
Args += basedOn;
}
}
}

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

@ -0,0 +1,41 @@
namespace SourceGit.Commands
{
public class Remote : Command
{
public Remote(string repo)
{
WorkingDirectory = repo;
Context = repo;
}
public bool Add(string name, string url)
{
Args = $"remote add {name} {url}";
return Exec();
}
public bool Delete(string name)
{
Args = $"remote remove {name}";
return Exec();
}
public bool Rename(string name, string to)
{
Args = $"remote rename {name} {to}";
return Exec();
}
public bool Prune(string name)
{
Args = $"remote prune {name}";
return Exec();
}
public bool SetURL(string name, string url)
{
Args = $"remote set-url {name} {url}";
return Exec();
}
}
}

38
src/Commands/Reset.cs Normal file
View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Reset : Command
{
public Reset(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "reset";
}
public Reset(string repo, List<Models.Change> changes)
{
WorkingDirectory = repo;
Context = repo;
var builder = new StringBuilder();
builder.Append("reset --");
foreach (var c in changes)
{
builder.Append(" \"");
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}";
}
}
}

23
src/Commands/Restore.cs Normal file
View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Restore : Command
{
public Restore(string repo, List<string> files, string extra)
{
WorkingDirectory = repo;
Context = repo;
StringBuilder builder = new StringBuilder();
builder.Append("restore ");
if (!string.IsNullOrEmpty(extra))
builder.Append(extra).Append(" ");
builder.Append("--");
foreach (var f in files)
builder.Append(' ').Append('"').Append(f).Append('"');
Args = builder.ToString();
}
}
}

14
src/Commands/Revert.cs Normal file
View file

@ -0,0 +1,14 @@
namespace SourceGit.Commands
{
public class Revert : Command
{
public Revert(string repo, string commit, bool autoCommit)
{
WorkingDirectory = repo;
Context = repo;
Args = $"revert {commit} --no-edit";
if (!autoCommit)
Args += " --no-commit";
}
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Avalonia.Threading;
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.GitInstallPath;
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)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Save change to patch failed: " + e.Message);
});
return false;
}
}
}
}

View file

@ -0,0 +1,81 @@
using System;
using System.Diagnostics;
using System.IO;
using Avalonia.Threading;
namespace SourceGit.Commands
{
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
{
ExecCmd(repo, $"show {revision}:\"{file}\"", saveTo);
}
}
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitInstallPath;
starter.Arguments = args;
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardInput = true;
starter.RedirectStandardOutput = true;
starter.RedirectStandardError = true;
using (var sw = File.OpenWrite(outputFile))
{
try
{
var proc = new Process() { StartInfo = starter };
proc.Start();
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)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, "Save file failed: " + e.Message);
});
return false;
}
}
}
}
}

82
src/Commands/Stash.cs Normal file
View file

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands
{
public class Stash : Command
{
public Stash(string repo)
{
WorkingDirectory = repo;
Context = repo;
}
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();
}
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;
}
public bool Apply(string name)
{
Args = $"stash apply -q {name}";
return Exec();
}
public bool Pop(string name)
{
Args = $"stash pop -q {name}";
return Exec();
}
public bool Drop(string name)
{
Args = $"stash drop -q {name}";
return Exec();
}
public bool Clear()
{
Args = "stash clear";
return Exec();
}
}
}

View file

@ -0,0 +1,39 @@
using System;
namespace SourceGit.Commands
{
public class Statistics : Command
{
public Statistics(string repo)
{
_statistics = new Models.Statistics();
WorkingDirectory = repo;
Context = repo;
Args = $"log --date-order --branches --remotes --since=\"{_statistics.Since()}\" --pretty=format:\"%ct$%cn\"";
}
public Models.Statistics Result()
{
Exec();
_statistics.Complete();
return _statistics;
}
protected override void OnReadline(string line)
{
var dateEndIdx = line.IndexOf('$', StringComparison.Ordinal);
if (dateEndIdx == -1)
return;
var dateStr = line.Substring(0, dateEndIdx);
var date = 0.0;
if (!double.TryParse(dateStr, out date))
return;
_statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
}
private readonly Models.Statistics _statistics = null;
}
}

55
src/Commands/Submodule.cs Normal file
View file

@ -0,0 +1,55 @@
using System;
namespace SourceGit.Commands
{
public class Submodule : Command
{
public Submodule(string repo)
{
WorkingDirectory = repo;
Context = repo;
}
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 -- {relativePath}";
return Exec();
}
else
{
Args = $"submodule update --init -- {relativePath}";
return true;
}
}
public bool Update()
{
Args = $"submodule update --rebase --remote";
return Exec();
}
public bool Delete(string relativePath)
{
Args = $"submodule deinit -f {relativePath}";
if (!Exec())
return false;
Args = $"rm -rf {relativePath}";
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

49
src/Commands/Tag.cs Normal file
View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands
{
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);
cmd.Args += $"-F \"{tmp}\"";
}
else
{
cmd.Args += $"-m {name}";
}
return cmd.Exec();
}
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 (remotes != null)
{
foreach (var r in remotes)
{
new Push(repo, r.Name, name, true).Exec();
}
}
return true;
}
}
}

19
src/Commands/Version.cs Normal file
View file

@ -0,0 +1,19 @@
namespace SourceGit.Commands
{
public class Version : Command
{
public Version()
{
Args = "--version";
RaiseError = false;
}
public string Query()
{
var rs = ReadToEnd();
if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut))
return string.Empty;
return rs.StdOut.Trim().Substring("git version ".Length);
}
}
}