Skip to content
55 changes: 13 additions & 42 deletions src/AL2DBML.CLI/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AL2DBML.CLI.Constants;
using AL2DBML.CLI.Models;
using AL2DBML.CLI.Services;
using Spectre.Console;
Expand All @@ -11,9 +12,6 @@ public class InitSettings : CommandSettings

public class InitCommand : AsyncCommand<InitSettings>
{
private const string HookStartMarker = "# [al2dbml-start]";
private const string HookEndMarker = "# [al2dbml-end]";
private const string HookCommand = "al2dbml generate";
private const string GitignoreEntry = ".al2dbml/config.local.json";

private readonly IConfigService _configService;
Expand Down Expand Up @@ -44,7 +42,7 @@ protected override Task<int> ExecuteAsync(CommandContext context, InitSettings s
.DefaultValue(existingShared?.Output.Name ?? "schema"));

var hookExists = File.Exists(".git/hooks/pre-commit") &&
File.ReadAllText(".git/hooks/pre-commit").Contains(HookStartMarker, StringComparison.Ordinal);
File.ReadAllText(".git/hooks/pre-commit").Contains(HookMarkers.Start, StringComparison.Ordinal);
var createHook = AnsiConsole.Confirm("Create a pre-commit hook?", hookExists);

_configService.SaveSharedConfig(new SharedConfig
Expand All @@ -59,7 +57,17 @@ protected override Task<int> ExecuteAsync(CommandContext context, InitSettings s
EnsureGitignoreEntry(GitignoreEntry);

if (createHook)
WritePreCommitHook();
{
if (!Directory.Exists(".git/hooks"))
AnsiConsole.MarkupLine("[yellow]Warning:[/] .git/hooks directory not found — skipping pre-commit hook creation.");
else
HookService.Write();
}
else if (hookExists)
{
AnsiConsole.MarkupLine("[yellow]Removing existing pre-commit hook...[/]");
HookService.Remove();
Comment on lines +68 to +69

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt asks "Create a pre-commit hook?", but when the user answers "No" and an AL2DBML hook already exists, the code removes the existing hook section. That’s a surprising/implicit behavior change (previously "No" would typically mean "leave as-is"). Consider either (a) changing the prompt text to reflect removal, or (b) adding a separate confirmation for removal / making "No" a no-op.

Suggested change
AnsiConsole.MarkupLine("[yellow]Removing existing pre-commit hook...[/]");
HookService.Remove();
var removeHook = AnsiConsole.Confirm(
"An existing AL2DBML pre-commit hook was found. Do you want to remove it?",
false);
if (removeHook)
{
AnsiConsole.MarkupLine("[yellow]Removing existing pre-commit hook...[/]");
HookService.Remove();
}

Copilot uses AI. Check for mistakes.
}

AnsiConsole.MarkupLine("[green]Done:[/] AL2DBML initialized.");
return Task.FromResult(0);
Expand All @@ -78,41 +86,4 @@ private static void EnsureGitignoreEntry(string entry)
lines.Add(entry);
File.WriteAllLines(gitignorePath, lines);
}

private static void WritePreCommitHook()
{
const string hookPath = ".git/hooks/pre-commit";

if (!Directory.Exists(".git/hooks"))
{
AnsiConsole.MarkupLine("[yellow]Warning:[/] .git/hooks directory not found — skipping pre-commit hook creation.");
return;
}
var hookSection = $"{HookStartMarker}\nif command -v al2dbml > /dev/null 2>&1; then\n {HookCommand} || printf \"\\033[33mWarning: al2dbml generate failed, skipping DBML update.\\033[0m\\n\"\nelse\n printf \"\\033[33mWarning: al2dbml not found, skipping DBML update.\\033[0m\\n\"\nfi\n{HookEndMarker}";

string content;
if (File.Exists(hookPath))
{
content = File.ReadAllText(hookPath);
var startIdx = content.IndexOf(HookStartMarker, StringComparison.Ordinal);
var endIdx = content.IndexOf(HookEndMarker, StringComparison.Ordinal);

if (startIdx >= 0 && endIdx >= 0)
content = content[..startIdx] + hookSection + content[(endIdx + HookEndMarker.Length)..];
else
content = content.TrimEnd() + $"\n\n{hookSection}\n";
}
else
{
content = $"#!/bin/sh\n\n{hookSection}\n";
}

File.WriteAllText(hookPath, content);

if (!OperatingSystem.IsWindows())
File.SetUnixFileMode(hookPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
}
23 changes: 23 additions & 0 deletions src/AL2DBML.CLI/Commands/RemoveHookCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using AL2DBML.CLI.Services;
using Spectre.Console;
using Spectre.Console.Cli;

namespace AL2DBML.CLI.Commands;

public class RemoveHookSettings : CommandSettings
{
}

public class RemoveHookCommand : Command<RemoveHookSettings>
{
protected override int Execute(CommandContext context, RemoveHookSettings settings, CancellationToken cancellationToken)
{
var removed = HookService.Remove();
if (!removed)
AnsiConsole.MarkupLine("[yellow]Warning:[/] No AL2DBML section found in pre-commit hook.");
else
AnsiConsole.MarkupLine("[green]Done:[/] AL2DBML section removed from pre-commit hook.");

return 0;
}
}
7 changes: 7 additions & 0 deletions src/AL2DBML.CLI/Constants/HookMarkers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace AL2DBML.CLI.Constants;

internal static class HookMarkers
{
public const string Start = "# [al2dbml-start]";
public const string End = "# [al2dbml-end]";
}
2 changes: 2 additions & 0 deletions src/AL2DBML.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
.AddAL2Dbml()
.AddScoped<GenerateCommand>()
.AddScoped<InitCommand>()
.AddScoped<RemoveHookCommand>()
.AddScoped<IParsingTracker, ParsingTracker>()
.AddScoped<IConfigService, ConfigService>();

Expand All @@ -23,6 +24,7 @@
{
config.AddCommand<GenerateCommand>("generate");
config.AddCommand<InitCommand>("init");
config.AddCommand<RemoveHookCommand>("remove-hook");
});

return await app.RunAsync(args);
Expand Down
65 changes: 65 additions & 0 deletions src/AL2DBML.CLI/Services/HookService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using AL2DBML.CLI.Constants;

namespace AL2DBML.CLI.Services;

internal static class HookService
{
private const string HookPath = ".git/hooks/pre-commit";
private const string HookCommand = "al2dbml generate";

public static void Write()
{
if (!Directory.Exists(".git/hooks"))
return;

var hookSection = $"{HookMarkers.Start}\nif command -v al2dbml > /dev/null 2>&1; then\n {HookCommand} || printf \"\\033[33mWarning: al2dbml generate failed, skipping DBML update.\\033[0m\\n\"\nelse\n printf \"\\033[33mWarning: al2dbml not found, skipping DBML update.\\033[0m\\n\"\nfi\n{HookMarkers.End}";

string content;
if (File.Exists(HookPath))
{
content = File.ReadAllText(HookPath);
var startIdx = content.IndexOf(HookMarkers.Start, StringComparison.Ordinal);
var endIdx = content.IndexOf(HookMarkers.End, StringComparison.Ordinal);

if (startIdx >= 0 && endIdx > startIdx)
content = content[..startIdx] + hookSection + content[(endIdx + HookMarkers.End.Length)..];
else
Comment on lines +20 to +26

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When replacing an existing hook section, the code only checks that both markers exist. If the end marker appears before the start marker (or the file contains multiple marker pairs), the slice math can remove/replace the wrong region. Consider validating endIdx > startIdx and (optionally) searching for the end marker starting from startIdx to ensure you match the correct pair.

Copilot uses AI. Check for mistakes.
content = content.TrimEnd() + $"\n\n{hookSection}\n";
}
else
{
content = $"#!/bin/sh\n\n{hookSection}\n";
}

File.WriteAllText(HookPath, content);

if (!OperatingSystem.IsWindows())
File.SetUnixFileMode(HookPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}

public static bool Remove()
{
if (!File.Exists(HookPath)) return false;

var content = File.ReadAllText(HookPath);
var startIdx = content.IndexOf(HookMarkers.Start, StringComparison.Ordinal);
var endIdx = content.IndexOf(HookMarkers.End, StringComparison.Ordinal);
if (startIdx < 0 || endIdx <= startIdx) return false;

var before = content[..startIdx].TrimEnd();
var after = content[(endIdx + HookMarkers.End.Length)..].TrimStart('\r', '\n');
Comment on lines +47 to +53

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove() removes the region using the first occurrences of the start/end markers without validating ordering. If the end marker occurs before the start marker, after will be computed from the wrong position and the resulting file content will be corrupted. Validate endIdx > startIdx (and ideally find the end marker after the start marker).

Copilot uses AI. Check for mistakes.
var newContent = before.Length > 0 && after.Length > 0
? before + "\n\n" + after
: (before + after).Trim();

if (string.IsNullOrWhiteSpace(newContent.Replace("#!/bin/sh", "")))
File.Delete(HookPath);
else
File.WriteAllText(HookPath, newContent + "\n");

return true;
}
}
Loading