diff --git a/Source/v2/Meadow.CLI.v2.sln b/Source/v2/Meadow.CLI.v2.sln index 7b5decdd..7c168378 100644 --- a/Source/v2/Meadow.CLI.v2.sln +++ b/Source/v2/Meadow.CLI.v2.sln @@ -40,7 +40,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Dfu", "Meadow.Dfu\Me EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Firmware", "Meadow.Firmware\Meadow.Firmware.csproj", "{D2274F30-A001-482A-99E3-0AB1970CF695}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Tooling.Core", "Meadow.Tooling.Core\Meadow.Tooling.Core.csproj", "{A22DBF4A-E472-445E-96C0-930C126039C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Tooling.Core", "Meadow.Tooling.Core\Meadow.Tooling.Core.csproj", "{A22DBF4A-E472-445E-96C0-930C126039C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Repository", "Meadow.Repository\Meadow.Repository.csproj", "{66E4476E-41C6-4618-BBC2-98C6277757E6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -48,6 +50,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {66E4476E-41C6-4618-BBC2-98C6277757E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66E4476E-41C6-4618-BBC2-98C6277757E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66E4476E-41C6-4618-BBC2-98C6277757E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66E4476E-41C6-4618-BBC2-98C6277757E6}.Release|Any CPU.Build.0 = Release|Any CPU {6C2FA084-701B-4A28-8775-BF18B84E366B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6C2FA084-701B-4A28-8775-BF18B84E366B}.Debug|Any CPU.Build.0 = Debug|Any CPU {6C2FA084-701B-4A28-8775-BF18B84E366B}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Source/v2/Meadow.CLI/Commands/Current/Config/ConfigRouteCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Config/ConfigRouteCommand.cs new file mode 100644 index 00000000..8a83acf8 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Config/ConfigRouteCommand.cs @@ -0,0 +1,24 @@ +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("config route", Description = "Sets the communication route for HCOM")] +public class ConfigRouteCommand : BaseSettingsCommand +{ + [CommandParameter(0, Name = "Route", IsRequired = true)] + public string Route { get; init; } + + public ConfigRouteCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { } + + + protected override ValueTask ExecuteCommand() + { + Logger?.LogInformation($"{Environment.NewLine}Setting route={Route}"); + SettingsManager.SaveSetting("route", Route); + + return ValueTask.CompletedTask; + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Config/ConfigSourceCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Config/ConfigSourceCommand.cs new file mode 100644 index 00000000..ea1d5d88 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Config/ConfigSourceCommand.cs @@ -0,0 +1,40 @@ +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("config source", Description = "Sets the root folder for Meadow source directories")] +public class ConfigSourceCommand : BaseSettingsCommand +{ + [CommandParameter(0, Name = "Root", IsRequired = false)] + public string? Root { get; init; } + + public ConfigSourceCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { } + + + protected override ValueTask ExecuteCommand() + { + var root = Root; + + // if Root is null, as the user if they want to use the current folder + if (string.IsNullOrWhiteSpace(Root)) + { + System.Console.Write("No root folder provided. You you want to use the current directory? (y/n) "); + if (System.Console.ReadLine()?.Trim() != "y") + { + System.Console.WriteLine("cancelled"); + return ValueTask.CompletedTask; + } + + root = Environment.CurrentDirectory; + } + + root!.Trim('\'').Trim('"'); ; + Logger?.LogInformation($"{Environment.NewLine}Setting source={root}"); + SettingsManager.SaveSetting("source", root!); + + return ValueTask.CompletedTask; + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Source/SourceCheckoutCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Source/SourceCheckoutCommand.cs new file mode 100644 index 00000000..ba8dfe75 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Source/SourceCheckoutCommand.cs @@ -0,0 +1,29 @@ +using CliFx.Attributes; +using Meadow.Tools; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("source checkout", Description = "Pulls a single-named branch of all Meadow source repositories")] +public class SourceCheckoutCommand : BaseCommand +{ + private ISettingsManager _settingsManager; + + [CommandParameter(0, Description = Strings.PathToMeadowProject, IsRequired = true)] + public string Branch { get; init; } + + public SourceCheckoutCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + var root = new MeadowRoot(_settingsManager); + + root.Checkout(Branch); + + return default; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/Source/SourceFetchCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Source/SourceFetchCommand.cs new file mode 100644 index 00000000..bd7b1e63 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Source/SourceFetchCommand.cs @@ -0,0 +1,26 @@ +using CliFx.Attributes; +using Meadow.Tools; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("source fetch", Description = "Fetches each of the local Meadow source repositories")] +public class SourceFetchCommand : BaseCommand +{ + private ISettingsManager _settingsManager; + + public SourceFetchCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + var root = new MeadowRoot(_settingsManager); + + root.Fetch(); + + return default; + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Source/SourcePullCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Source/SourcePullCommand.cs new file mode 100644 index 00000000..1b4207d0 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Source/SourcePullCommand.cs @@ -0,0 +1,47 @@ +using CliFx.Attributes; +using Meadow.Tools; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("source clone", Description = "Clones any missing local Meadow source repositories")] +public class SourceCloneCommand : BaseCommand +{ + private ISettingsManager _settingsManager; + + public SourceCloneCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + var root = new MeadowRoot(_settingsManager); + + root.Clone(); + + return default; + } +} + +[Command("source pull", Description = "Pulls each of the local Meadow source repositories")] +public class SourcePullCommand : BaseCommand +{ + private ISettingsManager _settingsManager; + + public SourcePullCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + var root = new MeadowRoot(_settingsManager); + + root.Pull(); + + return default; + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Source/SourceStatusCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Source/SourceStatusCommand.cs new file mode 100644 index 00000000..457b37f0 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Source/SourceStatusCommand.cs @@ -0,0 +1,26 @@ +using CliFx.Attributes; +using Meadow.Tools; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("source status", Description = "Compares the local Meadow source repositories with the remotes")] +public class SourceStatusCommand : BaseCommand +{ + private ISettingsManager _settingsManager; + + public SourceStatusCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + var root = new MeadowRoot(_settingsManager); + + root.Status(); + + return default; + } +} diff --git a/Source/v2/Meadow.Cli/Meadow.CLI.csproj b/Source/v2/Meadow.Cli/Meadow.CLI.csproj index 47f3eeea..f9f5cc01 100644 --- a/Source/v2/Meadow.Cli/Meadow.CLI.csproj +++ b/Source/v2/Meadow.Cli/Meadow.CLI.csproj @@ -45,6 +45,7 @@ + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 2075af16..f512d1f5 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -307,6 +307,30 @@ "commandName": "Project", "commandLineArgs": "flash os" }, + "source status": { + "commandName": "Project", + "commandLineArgs": "source status" + }, + "source checkout": { + "commandName": "Project", + "commandLineArgs": "source checkout develop" + }, + "source fetch": { + "commandName": "Project", + "commandLineArgs": "source fetch" + }, + "source pull": { + "commandName": "Project", + "commandLineArgs": "source pull" + }, + "source clone": { + "commandName": "Project", + "commandLineArgs": "source clone" + }, + "config source": { + "commandName": "Project", + "commandLineArgs": "config source 'C:\\repos\\wilderness'" + }, "WSL": { "commandName": "WSL2", "distributionName": "" diff --git a/Source/v2/Meadow.Repository/Meadow.Repository.csproj b/Source/v2/Meadow.Repository/Meadow.Repository.csproj new file mode 100644 index 00000000..2b9b2b13 --- /dev/null +++ b/Source/v2/Meadow.Repository/Meadow.Repository.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Source/v2/Meadow.Repository/MeadowRoot.cs b/Source/v2/Meadow.Repository/MeadowRoot.cs new file mode 100644 index 00000000..12fe35d5 --- /dev/null +++ b/Source/v2/Meadow.Repository/MeadowRoot.cs @@ -0,0 +1,154 @@ +using Meadow.CLI; + +namespace Meadow.Tools; + +public class MeadowRoot +{ + private const int NameWidth = 30; + private const int PropertyWidth = 6; + + private Repo[] _repositories; + + private string[] DefaultRepos = new string[] + { + "Meadow.Core", + "Meadow.Contracts", + "Meadow.Units", + "Meadow.Foundation", + "Meadow.Logging", + "Meadow.Modbus", + "Meadow.Samples", + "MQTTnet", + "Maple", + }; + + public DirectoryInfo Directory { get; } + + public MeadowRoot(ISettingsManager settingsManager) + { + var settings = settingsManager.GetPublicSettings(); + if (!settings.ContainsKey("source")) + { + throw new Exception("Source root folder not set. Use `meadow config source`"); + } + + var path = settings["source"].Trim('\'').Trim('"'); + Directory = new DirectoryInfo(path); + + if (!Directory.Exists) + { + Directory.Create(); + } + + // look for a file called "repos" in the root. If it exists, its contents will override the repo list + var overrideFile = Directory.GetFiles("repos").FirstOrDefault(); + if (overrideFile != null) + { + var repos = File.ReadAllLines(overrideFile.FullName).Where(l => l.Length > 0).ToArray(); + _repositories = new Repo[repos.Length]; + for (var i = 0; i < repos.Length; i++) + { + _repositories[i] = new Repo(Path.Combine(Directory.FullName, repos[i])); + } + } + else + { + _repositories = new Repo[DefaultRepos.Length]; + for (var i = 0; i < DefaultRepos.Length; i++) + { + _repositories[i] = new Repo(Path.Combine(Directory.FullName, DefaultRepos[i])); + } + } + } + + public void Clone() + { + foreach (var repo in _repositories) + { + // only clone if we don't have it + if (Path.Exists(repo.Folder)) + { + Console.WriteLine($"[{repo.Name}] already exists", ConsoleColor.White); + } + else + { + Console.WriteLine($"cloning [{repo.Name}]...", ConsoleColor.White); + repo.Clone(); + } + } + } + + public void Pull() + { + foreach (var repo in _repositories) + { + Console.WriteLine($"pulling [{repo.Name}]...", ConsoleColor.White); + repo.Pull(); + } + } + + public void Fetch() + { + foreach (var repo in _repositories) + { + Console.WriteLine($"fetching [{repo.Name}]...", ConsoleColor.White); + repo.Fetch(); + } + } + + public void Checkout(string branch) + { + Console.WriteLine($"Checking out [{branch}]...", ConsoleColor.White); + + foreach (var repo in _repositories) + { + Console.WriteLine($" repo [{repo.Name}]...", ConsoleColor.White); + repo.Checkout(branch); + } + } + + public void Status() + { + Console.WriteLine($"Source root is '{Directory.FullName}{Environment.NewLine}", ConsoleColor.White); + + Console.WriteLine($"| {"Repo name".PadRight(NameWidth)} | {"Current branch".PadRight(NameWidth)} | {"Ahead".PadRight(PropertyWidth)} | {"Behind".PadRight(PropertyWidth)} | {"Dirty".PadRight(PropertyWidth)} |"); + Console.WriteLine($"| {"".PadRight(NameWidth, '-')} | {"".PadRight(NameWidth, '-')} | {"".PadRight(PropertyWidth, '-')} | {"".PadRight(PropertyWidth, '-')} | {"".PadRight(PropertyWidth, '-')} |"); + + if (_repositories.Length == 0) + { + Console.WriteLine("| No git repos found"); + return; + } + + foreach (var repo in _repositories) + { + var name = repo.Name.PadRight(NameWidth); + var friendly = repo.Branch.PadRight(NameWidth); + var ahead = $"{repo.Ahead}".PadRight(PropertyWidth); + var behind = $"{repo.Behind}".PadRight(PropertyWidth); + var dirty = $"{repo.IsDirty}".PadRight(PropertyWidth); + + Console.Write("| "); + ConsoleWriteWithColor(name, ConsoleColor.White); + ConsoleWriteWithColor(friendly, ahead[0] == ' ' ? ConsoleColor.Yellow : ConsoleColor.White); + ConsoleWriteWithColor(ahead, ahead[0] == '0' ? ConsoleColor.White : ConsoleColor.Cyan); + ConsoleWriteWithColor(behind, behind[0] == '0' ? ConsoleColor.White : ConsoleColor.Cyan); + ConsoleWriteWithColor(dirty, repo.IsDirty ? ConsoleColor.Red : ConsoleColor.White); + Console.WriteLine(); + } + + } + + private void ConsoleWriteWithColor(string text, ConsoleColor color) + { + if (text.Length > NameWidth) + { + text = string.Concat(text.AsSpan(0, NameWidth - 3), "..."); + } + + Console.ForegroundColor = color; + Console.Write(text); + Console.ForegroundColor = ConsoleColor.Gray; + Console.Write(" | "); + } +} diff --git a/Source/v2/Meadow.Repository/Repo.cs b/Source/v2/Meadow.Repository/Repo.cs new file mode 100644 index 00000000..b32236bb --- /dev/null +++ b/Source/v2/Meadow.Repository/Repo.cs @@ -0,0 +1,131 @@ +using LibGit2Sharp; + +namespace Meadow.Tools; + +internal class Repo +{ + public string Name { get; set; } + + public string Folder { get; protected set; } + + public bool IsGitRepo { get; protected set; } = false; + + public string Branch { get; set; } + + public bool IsPrivate { get; set; } + + public bool HasRemote { get; set; } = false; + + public int? Ahead { get; set; } + + public int? Behind { get; set; } + + public bool IsDirty { get; protected set; } + + public Repo(string folder) + { + Folder = folder; + Name = Path.GetFileName(folder); + + Initialize(); + } + + private void Initialize() + { + try + { + using var repo = new Repository(Folder); + + IsGitRepo = true; + + Branch = repo.Head.FriendlyName; + + HasRemote = repo.Head.IsTracking; + + Ahead = repo.Head.TrackingDetails.AheadBy; + Behind = repo.Head.TrackingDetails.BehindBy; + IsDirty = repo.RetrieveStatus().IsDirty; + } + catch + { + } + } + + public bool Checkout(string branch) + { + using var repo = new Repository(Folder); + + Branch newBranch; + + try + { + newBranch = Commands.Checkout(repo, branch); + Initialize(); + } + catch (Exception ex) + { + Console.Write($"{ex.Message} "); + return false; + } + + return newBranch != null; + } + + public bool Pull() + { + if (IsPrivate || HasRemote == false) + { + return false; + } + + var options = new LibGit2Sharp.PullOptions(); + var signature = new Signature("meadow", "foo@noname.com", DateTimeOffset.Now); + + // var signature = new Signature(new Identity(Secrets.UserName, Secrets.Email), DateTimeOffset.Now); + + using var repo = new Repository(Folder); + Commands.Pull(repo, signature, options); + + return true; + } + + public bool Clone() + { + var options = new CloneOptions + { + Checkout = true, + BranchName = "develop" + }; + var url = $"https://github.com/WildernessLabs/{Path.GetFileName(Folder)}.git"; + + Repository.Clone(url, Folder, options); + + return true; + } + + public bool Fetch() + { + if (IsPrivate || HasRemote == false) + { + return false; + } + + using var repo = new Repository(Folder); + + foreach (Remote remote in repo.Network.Remotes) + { + try + { + FetchOptions options = new FetchOptions(); + IEnumerable refSpecs = remote.FetchRefSpecs.Select(x => x.Specification); + Commands.Fetch(repo, remote.Name, refSpecs, options, ""); + } + catch + { + return false; + } + } + + return true; + } +}