mirror of
https://github.com/FalloutCollaborationProject/FCP-Mod-Updater.git
synced 2026-07-27 17:04:05 -07:00
Misc
This commit is contained in:
+1
-10
@@ -23,17 +23,8 @@ public class ScanCommand : AsyncCommand<ModPathSettings>
|
|||||||
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
|
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
|
||||||
AnsiConsole.WriteLine();
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
// Check for git
|
|
||||||
var gitService = new GitService();
|
|
||||||
if (!await gitService.IsGitInstalledAsync(cancellationToken))
|
|
||||||
{
|
|
||||||
AnsiConsole.MarkupLine("[yellow]Warning: Git is not installed or not in PATH.[/]");
|
|
||||||
AnsiConsole.MarkupLine("[yellow]Git-based features will be limited.[/]");
|
|
||||||
AnsiConsole.MarkupLine("[grey]Install git from: https://git-scm.com/downloads[/]");
|
|
||||||
AnsiConsole.WriteLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
|
var gitService = new GitService();
|
||||||
var gitHubApiService = new GitHubApiService();
|
var gitHubApiService = new GitHubApiService();
|
||||||
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
|
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ using FCPModUpdater.Commands.Settings;
|
|||||||
using FCPModUpdater.Models;
|
using FCPModUpdater.Models;
|
||||||
using FCPModUpdater.Services;
|
using FCPModUpdater.Services;
|
||||||
using FCPModUpdater.UI;
|
using FCPModUpdater.UI;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using Spectre.Console.Cli;
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
namespace FCPModUpdater.Commands;
|
namespace FCPModUpdater.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
public class UpdateCommand : AsyncCommand<ModPathSettings>
|
public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||||
{
|
{
|
||||||
public override async Task<int> ExecuteAsync(CommandContext context, ModPathSettings settings,
|
public override async Task<int> ExecuteAsync(CommandContext context, ModPathSettings settings,
|
||||||
@@ -24,16 +26,8 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
|||||||
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
|
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
|
||||||
AnsiConsole.WriteLine();
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
// Check for git
|
|
||||||
var gitService = new GitService();
|
|
||||||
if (!await gitService.IsGitInstalledAsync(cancellationToken))
|
|
||||||
{
|
|
||||||
AnsiConsole.MarkupLine("[red]Error: Git is not installed or not in PATH.[/]");
|
|
||||||
AnsiConsole.MarkupLine("[grey]Install git from: https://git-scm.com/downloads[/]");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
|
var gitService = new GitService();
|
||||||
var gitHubApiService = new GitHubApiService();
|
var gitHubApiService = new GitHubApiService();
|
||||||
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
|
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
|
||||||
|
|
||||||
@@ -54,7 +48,7 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
|||||||
}
|
}
|
||||||
|
|
||||||
AnsiConsole.MarkupLine($"[yellow]Found {updateableMods.Count} mod(s) with updates available:[/]");
|
AnsiConsole.MarkupLine($"[yellow]Found {updateableMods.Count} mod(s) with updates available:[/]");
|
||||||
foreach (var mod in updateableMods)
|
foreach (InstalledMod mod in updateableMods)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine($" • {mod.Name} [grey]({mod.CommitsBehind} commits behind)[/]");
|
AnsiConsole.MarkupLine($" • {mod.Name} [grey]({mod.CommitsBehind} commits behind)[/]");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -15,6 +15,5 @@ public enum ModStatus
|
|||||||
public enum ModSource
|
public enum ModSource
|
||||||
{
|
{
|
||||||
Git,
|
Git,
|
||||||
Local,
|
Local
|
||||||
Workshop
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-12
@@ -6,18 +6,6 @@ namespace FCPModUpdater.Services;
|
|||||||
|
|
||||||
public class GitService : IGitService
|
public class GitService : IGitService
|
||||||
{
|
{
|
||||||
private bool? _gitInstalled;
|
|
||||||
|
|
||||||
public async Task<bool> IsGitInstalledAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (_gitInstalled.HasValue)
|
|
||||||
return _gitInstalled.Value;
|
|
||||||
|
|
||||||
var (exitCode, _, _) = await RunGitCommandAsync(".", "--version", ct);
|
|
||||||
_gitInstalled = exitCode == 0;
|
|
||||||
return _gitInstalled.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsGitRepositoryAsync(string path, CancellationToken ct = default)
|
public async Task<bool> IsGitRepositoryAsync(string path, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, _, _) = await RunGitCommandAsync(path, "rev-parse --is-inside-work-tree", ct);
|
var (exitCode, _, _) = await RunGitCommandAsync(path, "rev-parse --is-inside-work-tree", ct);
|
||||||
@@ -214,6 +202,8 @@ public class GitService : IGitService
|
|||||||
BufferedCommandResult result = await Cli.Wrap("git")
|
BufferedCommandResult result = await Cli.Wrap("git")
|
||||||
.WithArguments(arguments)
|
.WithArguments(arguments)
|
||||||
.WithWorkingDirectory(workingDirectory)
|
.WithWorkingDirectory(workingDirectory)
|
||||||
|
.WithEnvironmentVariables(env => env
|
||||||
|
.Set("GIT_TERMINAL_PROMPT", "0")) // Disable interactive prompts
|
||||||
.WithValidation(CommandResultValidation.None)
|
.WithValidation(CommandResultValidation.None)
|
||||||
.ExecuteBufferedAsync(cts.Token);
|
.ExecuteBufferedAsync(cts.Token);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace FCPModUpdater.Services;
|
|||||||
|
|
||||||
public interface IGitService
|
public interface IGitService
|
||||||
{
|
{
|
||||||
Task<bool> IsGitInstalledAsync(CancellationToken ct = default);
|
|
||||||
Task<bool> IsGitRepositoryAsync(string path, CancellationToken ct = default);
|
Task<bool> IsGitRepositoryAsync(string path, CancellationToken ct = default);
|
||||||
Task<string?> GetRemoteUrlAsync(string path, CancellationToken ct = default);
|
Task<string?> GetRemoteUrlAsync(string path, CancellationToken ct = default);
|
||||||
Task<GitCommitInfo?> GetCurrentCommitAsync(string path, CancellationToken ct = default);
|
Task<GitCommitInfo?> GetCurrentCommitAsync(string path, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace FCPModUpdater.Services;
|
|||||||
public class ModDiscoveryService : IModDiscoveryService
|
public class ModDiscoveryService : IModDiscoveryService
|
||||||
{
|
{
|
||||||
private const string FcpOrgUrl = "github.com/FalloutCollaborationProject/";
|
private const string FcpOrgUrl = "github.com/FalloutCollaborationProject/";
|
||||||
|
private static readonly string[] BranchSuffixes = ["-main", "-master", "-develop"];
|
||||||
|
|
||||||
private readonly IGitService _gitService;
|
private readonly IGitService _gitService;
|
||||||
private readonly IGitHubApiService _gitHubApiService;
|
private readonly IGitHubApiService _gitHubApiService;
|
||||||
@@ -25,7 +26,6 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var gitInstalled = await _gitService.IsGitInstalledAsync(ct);
|
|
||||||
var orgRepos = await _gitHubApiService.GetOrganizationReposAsync(ct);
|
var orgRepos = await _gitHubApiService.GetOrganizationReposAsync(ct);
|
||||||
var orgRepoNames = orgRepos.Select(r => r.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var orgRepoNames = orgRepos.Select(r => r.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
var folderName = Path.GetFileName(dir);
|
var folderName = Path.GetFileName(dir);
|
||||||
progress?.Report($"Scanning: {folderName}");
|
progress?.Report($"Scanning: {folderName}");
|
||||||
|
|
||||||
InstalledMod? mod = await AnalyzeModDirectoryAsync(dir, folderName, gitInstalled, orgRepoNames, ct);
|
InstalledMod? mod = await AnalyzeModDirectoryAsync(dir, folderName, orgRepoNames, ct);
|
||||||
if (mod != null)
|
if (mod != null)
|
||||||
{
|
{
|
||||||
mods.Add(mod);
|
mods.Add(mod);
|
||||||
@@ -52,46 +52,24 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
private async Task<InstalledMod?> AnalyzeModDirectoryAsync(
|
private async Task<InstalledMod?> AnalyzeModDirectoryAsync(
|
||||||
string path,
|
string path,
|
||||||
string folderName,
|
string folderName,
|
||||||
bool gitInstalled,
|
|
||||||
HashSet<string> orgRepoNames,
|
HashSet<string> orgRepoNames,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Check if it's a Workshop mod (has PublishedFileId.txt)
|
|
||||||
var isWorkshop = File.Exists(Path.Combine(path, "About", "PublishedFileId.txt"));
|
|
||||||
|
|
||||||
if (!gitInstalled)
|
|
||||||
{
|
|
||||||
// Can't check git status without git installed
|
|
||||||
// Check if folder name matches an org repo
|
|
||||||
if (orgRepoNames.Contains(folderName))
|
|
||||||
{
|
|
||||||
return new InstalledMod
|
|
||||||
{
|
|
||||||
Name = folderName,
|
|
||||||
Path = path,
|
|
||||||
Source = isWorkshop ? ModSource.Workshop : ModSource.Local,
|
|
||||||
Status = ModStatus.NonGit,
|
|
||||||
MatchedRepoName = folderName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isGitRepo = await _gitService.IsGitRepositoryAsync(path, ct);
|
var isGitRepo = await _gitService.IsGitRepositoryAsync(path, ct);
|
||||||
|
|
||||||
if (!isGitRepo)
|
if (!isGitRepo)
|
||||||
{
|
{
|
||||||
// Not a git repo - check if it matches an org repo by name
|
// Not a git repo - check if it matches an org repo by name
|
||||||
if (orgRepoNames.Contains(folderName))
|
var matchedRepo = MatchRepoName(folderName, orgRepoNames);
|
||||||
|
if (matchedRepo != null)
|
||||||
{
|
{
|
||||||
return new InstalledMod
|
return new InstalledMod
|
||||||
{
|
{
|
||||||
Name = folderName,
|
Name = folderName,
|
||||||
Path = path,
|
Path = path,
|
||||||
Source = isWorkshop ? ModSource.Workshop : ModSource.Local,
|
Source = ModSource.Local,
|
||||||
Status = ModStatus.NonGit,
|
Status = ModStatus.NonGit,
|
||||||
MatchedRepoName = folderName
|
MatchedRepoName = matchedRepo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +82,8 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
if (string.IsNullOrEmpty(remoteUrl))
|
if (string.IsNullOrEmpty(remoteUrl))
|
||||||
{
|
{
|
||||||
// Git repo without remote - check if folder matches org repo
|
// Git repo without remote - check if folder matches org repo
|
||||||
if (orgRepoNames.Contains(folderName))
|
var matchedRepo = MatchRepoName(folderName, orgRepoNames);
|
||||||
|
if (matchedRepo != null)
|
||||||
{
|
{
|
||||||
return new InstalledMod
|
return new InstalledMod
|
||||||
{
|
{
|
||||||
@@ -113,7 +92,7 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
Source = ModSource.Git,
|
Source = ModSource.Git,
|
||||||
Status = ModStatus.Error,
|
Status = ModStatus.Error,
|
||||||
ErrorMessage = "Git repository has no remote configured",
|
ErrorMessage = "Git repository has no remote configured",
|
||||||
MatchedRepoName = folderName
|
MatchedRepoName = matchedRepo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +104,7 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
if (!isFcpMod)
|
if (!isFcpMod)
|
||||||
{
|
{
|
||||||
// Check if folder name matches org repo (might be cloned from fork)
|
// Check if folder name matches org repo (might be cloned from fork)
|
||||||
if (!orgRepoNames.Contains(folderName))
|
if (MatchRepoName(folderName, orgRepoNames) == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -182,6 +161,29 @@ public class ModDiscoveryService : IModDiscoveryService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to match a folder name to an org repo, accounting for GitHub ZIP download suffixes like -main, -master.
|
||||||
|
/// </summary>
|
||||||
|
private static string? MatchRepoName(string folderName, HashSet<string> orgRepoNames)
|
||||||
|
{
|
||||||
|
// Direct match
|
||||||
|
if (orgRepoNames.Contains(folderName))
|
||||||
|
return folderName;
|
||||||
|
|
||||||
|
// Try stripping common branch suffixes (for ZIP downloads)
|
||||||
|
foreach (var suffix in BranchSuffixes)
|
||||||
|
{
|
||||||
|
if (folderName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var baseName = folderName[..^suffix.Length];
|
||||||
|
if (orgRepoNames.Contains(baseName))
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static ModStatus DetermineStatus(int behind, int ahead, bool hasLocalChanges, string? branch)
|
private static ModStatus DetermineStatus(int behind, int ahead, bool hasLocalChanges, string? branch)
|
||||||
{
|
{
|
||||||
if (branch == null)
|
if (branch == null)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public static class ModTableRenderer
|
|||||||
.AddColumn(new TableColumn("[bold]Commit[/]").Centered())
|
.AddColumn(new TableColumn("[bold]Commit[/]").Centered())
|
||||||
.AddColumn(new TableColumn("[bold]Status[/]").Centered());
|
.AddColumn(new TableColumn("[bold]Status[/]").Centered());
|
||||||
|
|
||||||
foreach (var mod in mods)
|
foreach (InstalledMod mod in mods)
|
||||||
{
|
{
|
||||||
table.AddRow(
|
table.AddRow(
|
||||||
FormatModName(mod),
|
FormatModName(mod),
|
||||||
@@ -48,15 +48,15 @@ public static class ModTableRenderer
|
|||||||
AnsiConsole.MarkupLine(
|
AnsiConsole.MarkupLine(
|
||||||
$"[grey]Summary:[/] [green]{upToDate} up to date[/] | [yellow]{behind} updates available[/] | [cyan]{localChanges} with local changes[/] | [grey]{nonGit} non-git[/]");
|
$"[grey]Summary:[/] [green]{upToDate} up to date[/] | [yellow]{behind} updates available[/] | [cyan]{localChanges} with local changes[/] | [grey]{nonGit} non-git[/]");
|
||||||
|
|
||||||
if (rateLimit.HasValue)
|
if (!rateLimit.HasValue)
|
||||||
{
|
return;
|
||||||
|
|
||||||
var resetTime = rateLimitReset.HasValue
|
var resetTime = rateLimitReset.HasValue
|
||||||
? $" (resets {rateLimitReset.Value.ToLocalTime():HH:mm})"
|
? $" (resets {rateLimitReset.Value.ToLocalTime():HH:mm})"
|
||||||
: "";
|
: "";
|
||||||
var color = rateLimit.Value < 10 ? "yellow" : "grey";
|
var color = rateLimit.Value < 10 ? "yellow" : "grey";
|
||||||
AnsiConsole.MarkupLine($"[{color}]GitHub API: {rateLimit.Value} requests remaining{resetTime}[/]");
|
AnsiConsole.MarkupLine($"[{color}]GitHub API: {rateLimit.Value} requests remaining{resetTime}[/]");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatModName(InstalledMod mod)
|
private static string FormatModName(InstalledMod mod)
|
||||||
{
|
{
|
||||||
@@ -75,7 +75,6 @@ public static class ModTableRenderer
|
|||||||
{
|
{
|
||||||
ModSource.Git => "[green]Git[/]",
|
ModSource.Git => "[green]Git[/]",
|
||||||
ModSource.Local => "[grey]Local[/]",
|
ModSource.Local => "[grey]Local[/]",
|
||||||
ModSource.Workshop => "[blue]Workshop[/]",
|
|
||||||
_ => "[grey]?[/]"
|
_ => "[grey]?[/]"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-11
@@ -61,12 +61,12 @@ public static class ProgressReporter
|
|||||||
new SpinnerColumn())
|
new SpinnerColumn())
|
||||||
.StartAsync(async ctx =>
|
.StartAsync(async ctx =>
|
||||||
{
|
{
|
||||||
var overallTask = ctx.AddTask($"[bold]{description}[/]", maxValue: items.Count);
|
ProgressTask overallTask = ctx.AddTask($"[bold]{description}[/]", maxValue: items.Count);
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (T item in items)
|
||||||
{
|
{
|
||||||
var name = nameSelector(item);
|
var name = nameSelector(item);
|
||||||
var itemTask = ctx.AddTask($" {name}");
|
ProgressTask itemTask = ctx.AddTask($" {name}");
|
||||||
|
|
||||||
var progress = new Progress<double>(p => itemTask.Value = p);
|
var progress = new Progress<double>(p => itemTask.Value = p);
|
||||||
|
|
||||||
@@ -76,14 +76,9 @@ public static class ProgressReporter
|
|||||||
results.Add((name, success, error));
|
results.Add((name, success, error));
|
||||||
itemTask.Value = 100;
|
itemTask.Value = 100;
|
||||||
|
|
||||||
if (!success)
|
itemTask.Description = success
|
||||||
{
|
? $" [green]{name}[/]"
|
||||||
itemTask.Description = $" [red]{name}[/]";
|
: $" [red]{name}[/]";
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
itemTask.Description = $" [green]{name}[/]";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user