Skip to content

Commit dde89ea

Browse files
committed
Simplify --init handling, make it cross-cutting
1 parent 9d50a3e commit dde89ea

3 files changed

Lines changed: 127 additions & 155 deletions

File tree

src/File/File.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<OutputType>Exe</OutputType>
77
<TargetFramework>net8.0</TargetFramework>
88
<RollForward>Major</RollForward>
9+
<LangVersion>Latest</LangVersion>
910

1011
<AssemblyName>file</AssemblyName>
1112
<RootNamespace>Devlooped</RootNamespace>

src/File/Program.cs

Lines changed: 124 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,127 +3,153 @@
33
using System.IO;
44
using System.Linq;
55
using System.Reflection;
6-
using System.Threading.Tasks;
6+
using ColoredConsole;
7+
using Devlooped;
78
using DotNetConfig;
89
using Mono.Options;
910

10-
namespace Devlooped;
11-
12-
class Program
13-
{
14-
static async Task<int> Main(string[] args)
15-
{
16-
var help = false;
17-
string? changelog = null;
18-
var options = new OptionSet
11+
var help = false;
12+
string? changelog = null;
13+
var options = new OptionSet
1914
{
2015
{ "?|h|help", "Display this help", h => help = h != null },
2116
{ "c|changelog:", "Write a changelog", c => changelog = string.IsNullOrEmpty(c) ? "dotnet-file.md" : c },
2217
};
2318

24-
var extraArgs = options.Parse(args);
25-
if ((args.Length == 1 && help) || extraArgs.Count == 0)
26-
return ShowHelp();
19+
var extraArgs = options.Parse(args);
20+
if ((args.Length == 1 && help) || extraArgs.Count == 0)
21+
return ShowHelp();
2722

28-
var config = Config.Build();
29-
var command = extraArgs[0].ToLowerInvariant() switch
30-
{
31-
"add" => new AddCommand(config),
32-
"changes" => new ChangesCommand(config),
33-
"delete" => new DeleteCommand(config),
34-
"init" => new InitCommand(config),
35-
"list" => new ListCommand(config),
36-
"sync" => new SyncCommand(config),
37-
"update" => new UpdateCommand(config),
38-
_ => Command.NullCommand,
39-
};
23+
var commandName = extraArgs[0].ToLowerInvariant();
24+
extraArgs.RemoveAt(0);
4025

41-
if (command == Command.NullCommand)
42-
return ShowHelp();
26+
var config = Config.Build();
4327

44-
// Remove first arg which is the command to use.
45-
extraArgs.RemoveAt(0);
28+
// Handle --init [url] as a cross-cutting pre-processing step: download a remote
29+
// .netconfig and merge its skip=true entries into the local config before any command
30+
// runs, so upstream skips are honored regardless of which command is executed.
31+
string? initUrl = null;
32+
if (extraArgs.FindIndex(a => a == "--init") is var initIndex && initIndex >= 0 &&
33+
initIndex + 1 < extraArgs.Count && !extraArgs[initIndex + 1].StartsWith('-'))
34+
{
35+
initUrl = extraArgs[initIndex + 1];
36+
extraArgs.RemoveRange(initIndex, 2);
4637

47-
// Handle --init [url] option for the sync command.
48-
if (command is SyncCommand sync &&
49-
extraArgs.FindIndex(a => a == "--init") is var index && index >= 0)
50-
{
51-
if (index + 1 < extraArgs.Count && !extraArgs[index + 1].StartsWith('-'))
52-
{
53-
sync.InitUrl = extraArgs[index + 1];
54-
extraArgs.RemoveRange(index, 2);
55-
}
56-
else
57-
{
58-
extraArgs.RemoveAt(index);
59-
}
60-
}
38+
var tempConfig = Path.GetTempFileName();
39+
var tempFile = Path.GetTempFileName();
40+
try
41+
{
42+
var addCmd = new AddCommand(Config.Build(tempConfig));
43+
addCmd.Files.Add(new FileSpec(tempFile, new Uri(initUrl)));
6144

62-
// Add remainder arguments as if they were just files or urls provided
63-
// to the command. Allows skipping the -f|-u switches.
64-
var skip = false;
65-
var files = new List<FileSpec>();
66-
List<FileSpec>? configured = default;
45+
ColorConsole.WriteLine("Downloading init config file(s)...".Yellow());
46+
await addCmd.ExecuteAsync();
6747

68-
for (var i = 0; i < extraArgs.Count; i++)
48+
var remoteConfig = Config.Build(tempFile);
49+
foreach (var group in remoteConfig
50+
.Where(x => x.Level == null && x.Section == "file" && x.Subsection != null)
51+
.GroupBy(x => x.Subsection!))
6952
{
70-
if (skip)
71-
{
72-
skip = false;
53+
var path = group.Key;
54+
if (remoteConfig.GetBoolean("file", path, "skip") != true)
7355
continue;
74-
}
75-
76-
// Try to pair Uri+File to allow intuitive download>path mapping, such as
77-
// https://gitub.com/org/repo/docs/file.md > docs/file.md
78-
if (Uri.TryCreate(extraArgs[i], UriKind.Absolute, out var uri))
79-
{
80-
var next = i + 1;
81-
// If the next arg is not a URI, use that as the file path for the uri
82-
if (next < extraArgs.Count && !Uri.TryCreate(extraArgs[next], UriKind.Absolute, out _))
83-
{
84-
files.Add(FileSpec.WithPath(extraArgs[next], uri));
85-
skip = true;
86-
}
87-
else
88-
{
89-
// If the next file is a URI, then no path has been specified.
90-
// We should attempt to recreate the same path structure locally,
91-
// which is the most intuitive default. If users don't want that,
92-
// they can specify '.' to get the old behavior.
93-
files.Add(FileSpec.WithDefaultPath(uri));
94-
}
95-
}
96-
else
97-
{
98-
// Attempt to match a simple filename to a configured one
99-
configured ??= [.. command.GetConfiguredFiles()];
100-
if (configured.FirstOrDefault(x => x.Path == extraArgs[i]) is { } spec)
101-
files.Add(spec);
102-
}
103-
}
10456

105-
command.Files.AddRange(files);
57+
// Local entry takes precedence — only add skip if no local entry exists
58+
if (config.Where(x => x.Section == "file" && x.Subsection == path).Any())
59+
continue;
10660

107-
var result = await command.ExecuteAsync();
61+
var url = remoteConfig.GetString("file", path, "url");
62+
if (url != null)
63+
config = config.SetString("file", path, "url", url);
10864

109-
// If there were changes and a changelog was requested, emit it
110-
// to a file.
111-
if (changelog != null &&
112-
command is AddCommand add &&
113-
add.Changes.Count > 0 &&
114-
GitHub.IsInstalled)
115-
{
116-
GitHub.WriteChanges(changelog!, add.Changes);
65+
config = config.SetBoolean("file", path, "skip", true);
11766
}
118-
119-
return result;
12067
}
68+
finally
69+
{
70+
File.Delete(tempConfig);
71+
File.Delete(tempFile);
72+
}
73+
}
12174

122-
static int ShowHelp()
75+
var command = commandName switch
76+
{
77+
"add" => new AddCommand(config),
78+
"changes" => new ChangesCommand(config),
79+
"delete" => new DeleteCommand(config),
80+
"init" => new InitCommand(config),
81+
"list" => new ListCommand(config),
82+
"sync" => new SyncCommand(config),
83+
"update" => new UpdateCommand(config),
84+
_ => Devlooped.Command.NullCommand,
85+
};
86+
87+
if (command == Devlooped.Command.NullCommand)
88+
return ShowHelp();
89+
90+
// Add remainder arguments as if they were just files or urls provided
91+
// to the command. Allows skipping the -f|-u switches.
92+
var skip = false;
93+
var files = new List<FileSpec>();
94+
List<FileSpec>? configured = default;
95+
96+
for (var i = 0; i < extraArgs.Count; i++)
97+
{
98+
if (skip)
12399
{
124-
Console.WriteLine(File.ReadAllText(
125-
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Help.txt")));
100+
skip = false;
101+
continue;
102+
}
126103

127-
return 0;
104+
// Try to pair Uri+File to allow intuitive download>path mapping, such as
105+
// https://gitub.com/org/repo/docs/file.md > docs/file.md
106+
if (Uri.TryCreate(extraArgs[i], UriKind.Absolute, out var uri))
107+
{
108+
var next = i + 1;
109+
// If the next arg is not a URI, use that as the file path for the uri
110+
if (next < extraArgs.Count && !Uri.TryCreate(extraArgs[next], UriKind.Absolute, out _))
111+
{
112+
files.Add(FileSpec.WithPath(extraArgs[next], uri));
113+
skip = true;
114+
}
115+
else
116+
{
117+
// If the next file is a URI, then no path has been specified.
118+
// We should attempt to recreate the same path structure locally,
119+
// which is the most intuitive default. If users don't want that,
120+
// they can specify '.' to get the old behavior.
121+
files.Add(FileSpec.WithDefaultPath(uri));
122+
}
123+
}
124+
else
125+
{
126+
// Attempt to match a simple filename to a configured one
127+
configured ??= [.. command.GetConfiguredFiles()];
128+
if (configured.FirstOrDefault(x => x.Path == extraArgs[i]) is { } spec)
129+
files.Add(spec);
128130
}
129131
}
132+
133+
command.Files.AddRange(files);
134+
135+
var result = await command.ExecuteAsync();
136+
137+
// If there were changes and a changelog was requested, emit it
138+
// to a file.
139+
if (changelog != null &&
140+
command is AddCommand add &&
141+
add.Changes.Count > 0 &&
142+
Devlooped.GitHub.IsInstalled)
143+
{
144+
Devlooped.GitHub.WriteChanges(changelog!, add.Changes);
145+
}
146+
147+
return result;
148+
149+
static int ShowHelp()
150+
{
151+
Console.WriteLine(File.ReadAllText(
152+
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Help.txt")));
153+
154+
return 0;
155+
}

src/File/SyncCommand.cs

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,12 @@
1-
using System;
2-
using System.IO;
1+
using System.IO;
32
using System.Linq;
4-
using System.Threading.Tasks;
5-
using ColoredConsole;
63
using DotNetConfig;
74

85
namespace Devlooped;
96

107
class SyncCommand(Config configuration) : UpdateCommand(configuration)
118
{
12-
/// <summary>
13-
/// Optional URL to a remote .netconfig whose skip=true entries are merged into
14-
/// the local config before the sync runs (local entries always take precedence).
15-
/// </summary>
16-
public string? InitUrl { get; set; }
17-
18-
public override async Task<int> ExecuteAsync()
19-
{
20-
if (InitUrl != null)
21-
{
22-
var tempConfig = Path.GetTempFileName();
23-
var tempFile = Path.GetTempFileName();
24-
try
25-
{
26-
var addCmd = new AddCommand(Config.Build(tempConfig));
27-
addCmd.Files.Add(new FileSpec(tempFile, new Uri(InitUrl)));
28-
29-
ColorConsole.WriteLine("Downloading init config file(s)...".Yellow());
30-
await addCmd.ExecuteAsync();
31-
32-
var remoteConfig = Config.Build(tempFile);
33-
var localConfig = Configuration;
34-
35-
foreach (var group in remoteConfig
36-
.Where(x => x.Level == null && x.Section == "file" && x.Subsection != null)
37-
.GroupBy(x => x.Subsection!))
38-
{
39-
var path = group.Key;
40-
if (remoteConfig.GetBoolean("file", path, "skip") != true)
41-
continue;
42-
43-
// Local entry takes precedence — only add skip if no local entry exists
44-
if (localConfig.Where(x => x.Section == "file" && x.Subsection == path).Any())
45-
continue;
46-
47-
var url = remoteConfig.GetString("file", path, "url");
48-
if (url != null)
49-
localConfig = localConfig.SetString("file", path, "url", url);
50-
51-
localConfig = localConfig.SetBoolean("file", path, "skip", true);
52-
}
53-
54-
Configuration = localConfig;
55-
}
56-
finally
57-
{
58-
File.Delete(tempConfig);
59-
File.Delete(tempFile);
60-
}
61-
}
62-
63-
return await base.ExecuteAsync();
64-
}
9+
protected override AddCommand Clone() => new SyncCommand(Configuration);
6510

6611
protected override bool OnRemoteUrlMissing(FileSpec spec)
6712
{

0 commit comments

Comments
 (0)