mirror of
https://github.com/nsnail/dot.git
synced 2025-11-05 03:20:48 +08:00
refactor: ♻️ 2.0 (#13)
This commit is contained in:
128
src/backend/Dot/Git/Main.cs
Normal file
128
src/backend/Dot/Git/Main.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using NSExt.Extensions;
|
||||
|
||||
namespace Dot.Git;
|
||||
|
||||
[Description(nameof(Ln.Git批量操作工具))]
|
||||
[Localization(typeof(Ln))]
|
||||
internal sealed class Main : ToolBase<Option>
|
||||
{
|
||||
private Encoding _gitOutputEnc; // git command rsp 编码
|
||||
private ConcurrentDictionary<string, StringBuilder> _repoRsp; // 仓库信息容器
|
||||
private ConcurrentDictionary<string, TaskStatusColumn.Statues> _repoStatus;
|
||||
|
||||
protected override Task CoreAsync()
|
||||
{
|
||||
return !Directory.Exists(Opt.Path)
|
||||
? throw new ArgumentException($"{Ln.指定的路径不存在}: {Opt.Path}", "PATH")
|
||||
: CoreInternalAsync();
|
||||
}
|
||||
|
||||
private async Task CoreInternalAsync()
|
||||
{
|
||||
_gitOutputEnc = Encoding.GetEncoding(Opt.Git输出编码);
|
||||
var progressBar = new ProgressBarColumn { Width = 10 };
|
||||
await AnsiConsole.Progress()
|
||||
.Columns( //
|
||||
progressBar //
|
||||
, new ElapsedTimeColumn() //
|
||||
, new SpinnerColumn() //
|
||||
, new TaskStatusColumn() //
|
||||
, new TaskDescriptionColumn { Alignment = Justify.Left }) //
|
||||
.StartAsync(ctx => {
|
||||
var taskFinder = ctx.AddTask($"{Ln.查找此目录下所有Git仓库目录}: {Opt.Path}").IsIndeterminate();
|
||||
var paths = Directory.GetDirectories(Opt.Path, ".git" //
|
||||
, new EnumerationOptions //
|
||||
{
|
||||
MaxRecursionDepth = Opt.MaxRecursionDepth
|
||||
, RecurseSubdirectories = true
|
||||
, IgnoreInaccessible = true
|
||||
, AttributesToSkip = FileAttributes.ReparsePoint
|
||||
})
|
||||
.Select(x => Directory.GetParent(x)!.FullName);
|
||||
|
||||
_repoRsp = new ConcurrentDictionary<string, StringBuilder>();
|
||||
_repoStatus = new ConcurrentDictionary<string, TaskStatusColumn.Statues>();
|
||||
var tasks = new Dictionary<string, ProgressTask>();
|
||||
foreach (var path in paths) {
|
||||
_ = _repoRsp.TryAdd(path, new StringBuilder());
|
||||
_ = _repoStatus.TryAdd(path, default);
|
||||
var task = ctx.AddTask(new DirectoryInfo(path).Name, false).IsIndeterminate();
|
||||
tasks.Add(path, task);
|
||||
}
|
||||
|
||||
taskFinder.StopTask();
|
||||
taskFinder.State.Status(TaskStatusColumn.Statues.Succeed);
|
||||
|
||||
return Parallel.ForEachAsync(tasks, DirHandleAsync);
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var table = new Table().AddColumn(new TableColumn(Ln.仓库) { Width = 50 })
|
||||
.AddColumn(new TableColumn(Ln.命令))
|
||||
.AddColumn(new TableColumn(Ln.响应) { Width = 50 })
|
||||
.Caption(
|
||||
$"{Ln.Git退出码为零的}: [green]{_repoStatus.Count(x => x.Value == TaskStatusColumn.Statues
|
||||
.Succeed)}[/]/{_repoStatus.Count}");
|
||||
|
||||
foreach (var repo in _repoRsp) {
|
||||
var status = _repoStatus[repo.Key].ResDesc<Ln>();
|
||||
_ = table.AddRow( //
|
||||
status.Replace(_repoStatus[repo.Key].ToString(), new DirectoryInfo(repo.Key).Name), Opt.Args
|
||||
, status.Replace(_repoStatus[repo.Key].ToString(), repo.Value.ToString()));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
|
||||
private async ValueTask DirHandleAsync(KeyValuePair<string, ProgressTask> payload, CancellationToken ct)
|
||||
{
|
||||
payload.Value.StartTask();
|
||||
payload.Value.State.Status(TaskStatusColumn.Statues.Executing);
|
||||
|
||||
// 打印 git command rsp
|
||||
void ExecRspReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (e.Data is null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = Encoding.UTF8.GetString(_gitOutputEnc.GetBytes(e.Data));
|
||||
_ = _repoRsp[payload.Key].Append(msg.EscapeMarkup());
|
||||
}
|
||||
|
||||
// 启动git进程
|
||||
var startInfo = new ProcessStartInfo {
|
||||
CreateNoWindow = true
|
||||
, WorkingDirectory = payload.Key
|
||||
, FileName = "git"
|
||||
, Arguments = Opt.Args
|
||||
, UseShellExecute = false
|
||||
, RedirectStandardOutput = true
|
||||
, RedirectStandardError = true
|
||||
};
|
||||
using var p = Process.Start(startInfo);
|
||||
p!.OutputDataReceived += ExecRspReceived;
|
||||
p.ErrorDataReceived += ExecRspReceived;
|
||||
p.BeginOutputReadLine();
|
||||
p.BeginErrorReadLine();
|
||||
await p.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (p.ExitCode == 0) {
|
||||
payload.Value.State.Status(TaskStatusColumn.Statues.Succeed);
|
||||
_ = _repoStatus.AddOrUpdate(payload.Key, _ => TaskStatusColumn.Statues.Succeed
|
||||
, (_, _) => TaskStatusColumn.Statues.Succeed);
|
||||
payload.Value.StopTask();
|
||||
}
|
||||
else {
|
||||
payload.Value.State.Status(TaskStatusColumn.Statues.Failed);
|
||||
_ = _repoStatus.AddOrUpdate(payload.Key, _ => TaskStatusColumn.Statues.Failed
|
||||
, (_, _) => TaskStatusColumn.Statues.Failed);
|
||||
}
|
||||
|
||||
payload.Value.StopTask();
|
||||
}
|
||||
}
|
||||
31
src/backend/Dot/Git/Option.cs
Normal file
31
src/backend/Dot/Git/Option.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Dot.Git;
|
||||
|
||||
internal sealed class Option : OptionBase
|
||||
{
|
||||
[CommandOption("-a|--args")]
|
||||
[Description(nameof(Ln.传递给Git的参数))]
|
||||
[Localization(typeof(Ln))]
|
||||
[DefaultValue("status")]
|
||||
public string Args { get; set; }
|
||||
|
||||
[CommandOption("-e|--git-output-encoding")]
|
||||
[Description(nameof(Ln.Git输出编码))]
|
||||
[Localization(typeof(Ln))]
|
||||
[DefaultValue("utf-8")]
|
||||
public string Git输出编码 { get; set; }
|
||||
|
||||
[CommandOption("-d|--max-recursion-depth")]
|
||||
[Description(nameof(Ln.目录检索深度))]
|
||||
[Localization(typeof(Ln))]
|
||||
[DefaultValue(int.MaxValue)]
|
||||
public int MaxRecursionDepth { get; set; }
|
||||
|
||||
[CommandArgument(0, "[PATH]")]
|
||||
[Description(nameof(Ln.要处理的目录路径))]
|
||||
[Localization(typeof(Ln))]
|
||||
[DefaultValue(".")]
|
||||
public string Path { get; set; }
|
||||
}
|
||||
9
src/backend/Dot/Git/ProgressTaskStateExtensions.cs
Normal file
9
src/backend/Dot/Git/ProgressTaskStateExtensions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Dot.Git;
|
||||
|
||||
internal static class ProgressTaskStateExtensions
|
||||
{
|
||||
public static void Status(this ProgressTaskState me, TaskStatusColumn.Statues value)
|
||||
{
|
||||
_ = me.Update<TaskStatusColumn.Statues>(nameof(TaskStatusColumn), _ => value);
|
||||
}
|
||||
}
|
||||
58
src/backend/Dot/Git/TaskStatusColumn.cs
Normal file
58
src/backend/Dot/Git/TaskStatusColumn.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
|
||||
|
||||
using NSExt.Extensions;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Dot.Git;
|
||||
|
||||
internal sealed class TaskStatusColumn : ProgressColumn
|
||||
{
|
||||
public enum Statues : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Ready
|
||||
/// </summary>
|
||||
[Description($"[gray]{nameof(Ready)}[/]")]
|
||||
Ready = 0
|
||||
|
||||
,
|
||||
|
||||
/// <summary>
|
||||
/// Executing
|
||||
/// </summary>
|
||||
[Description($"[yellow]{nameof(Executing)}[/]")]
|
||||
Executing = 1
|
||||
|
||||
,
|
||||
|
||||
/// <summary>
|
||||
/// Succeed
|
||||
/// </summary>
|
||||
[Description($"[green]{nameof(Succeed)}[/]")]
|
||||
Succeed = 2
|
||||
|
||||
,
|
||||
|
||||
/// <summary>
|
||||
/// Failed
|
||||
/// </summary>
|
||||
[Description($"[red]{nameof(Failed)}[/]")]
|
||||
Failed = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the task description.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The alignment of the task description.
|
||||
/// </value>
|
||||
public Justify Alignment { get; set; } = Justify.Right;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||
{
|
||||
var text = task.State.Get<Statues>(nameof(TaskStatusColumn));
|
||||
return new Markup(text.ResDesc<Ln>()).Overflow(Overflow.Ellipsis).Justify(Alignment);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user