
dotnet-gitmoji: un CLI de gitmoji para el mundo dotnet
Una herramienta .NET que lleva la convención de commits de gitmoji a Git, con un hook prepare-commit-msg administrado por Husky.Net para equipos y un modo cliente interactivo para flujos personales.
Descripción del proyecto
dotnet-gitmoji es una pequeña herramienta de .NET 10 que lleva la convención de commits de gitmoji a un flujo .NET. Puedes instalarla globalmente para uso personal o fijarla en el tool manifest local de un repositorio para uso en equipo. Desde ahí tienes dos caminos de adopción: un hook prepare-commit-msg administrado a través de Husky.Net, o dotnet-gitmoji commit como reemplazo interactivo del comando nativo de Git. En cualquiera de los dos caminos, la herramienta te pide un gitmoji, te permite hacer fuzzy search sobre la lista completa y formatea el mensaje de commit final.
La forma que más me interesa es el flujo de equipo. Cuando un repositorio deja versionados .config/dotnet-tools.json, .husky/ y Directory.Build.targets, un integrante puede clonarlo, ejecutar dotnet restore o abrir la solución en Visual Studio o Rider, y obtener el mismo prompt de gitmoji en el siguiente commit. Así toda la convención se queda dentro de NuGet, MSBuild y el SDK de .NET en lugar de depender de Node.js sólo para compartir un hook de commit.
Tecnologías utilizadas
| Capa | Elección | Por qué está en el stack |
|---|---|---|
| Runtime | .NET 10 | Target moderno con soporte de primera para global tools y una línea base estable de System.Text.Json. |
| Empaquetado | .NET tool | Se publica en NuGet como dotnet-gitmoji y funciona tanto como herramienta local compartida como instalación global personal. |
| CLI framework | CliFx | Atributos declarativos para comandos y opciones, y juega bien con DI sin configuración adicional. |
| UX de terminal | Spectre.Console | Manejo de prompts de selección, pickers con fuzzy search en vivo y salida con color. |
| Invocación de procesos | CliWrap | Envuelve cada llamada a git y dotnet con una API tipada y amigable para tests. |
| Composición | Microsoft.Extensions.DependencyInjection | Mantiene servicios, validadores y comandos cableados a través de un solo contenedor. |
| HTTP | Microsoft.Extensions.Http | Trae la lista de gitmoji desde gitmoji.dev con un HttpClient tipado. |
| Testing | xUnit, NSubstitute, coverlet | Tests unitarios para servicios y validadores, además de un fixture de integración para la herramienta de extremo a extremo. |
Arquitectura
La herramienta sigue la estructura estándar de CliFx. Program.cs configura el contenedor de DI, CliFx resuelve el comando pedido desde el service provider, y el comando delega el trabajo real a un servicio. Los servicios son interface-first, y eso es lo que hace que los tests con xUnit y NSubstitute sean fáciles de escribir. Al mismo flujo de commit se accede desde dos puntos de entrada: el hook prepare-commit-msg de Git y el comando interactivo commit.
flowchart LR
subgraph CLI["CliFx commands"]
MAIN["Program.cs\nDI setup + app entry"]
CLIENT_CMDS["commit\nlist · search\nconfig · update"]
HOOK_CMDS["hook\ninit · remove"]
end
subgraph CORE["Shared core"]
GIT["GitService\nhook detect/install\nstage changes"]
PROMPT["PromptService\nfuzzy selector UI"]
FUZZY["GitmojiFuzzyMatcher\nscoring & ranking"]
CONFIG["ConfigurationService\n.gitmojirc.json → global"]
VALIDATOR["CommitMessageValidator\nparse existing message"]
end
subgraph DATA["Data & presentation"]
PROVIDER["GitmojiProvider\nAPI → cache → embedded"]
MODELS["Gitmoji · ToolConfiguration\nEmojiFormat · ValidationResult"]
end
MAIN --> CLIENT_CMDS
MAIN --> HOOK_CMDS
CLIENT_CMDS --> PROMPT
CLIENT_CMDS --> PROVIDER
CLIENT_CMDS --> CONFIG
HOOK_CMDS --> GIT
HOOK_CMDS --> PROMPT
HOOK_CMDS --> PROVIDER
HOOK_CMDS --> VALIDATOR
PROMPT --> FUZZY
FUZZY --> MODELS
PROVIDER --> MODELS
CONFIG --> MODELS
El diagrama de arquitectura muestra de qué se encarga cada capa. El diagrama de secuencia de abajo muestra qué se ejecuta realmente al hacer un commit. Ambos puntos de entrada convergen en PromptService, que controla el selector difuso (fuzzy) y luego delega en CommitMessageService la escritura del resultado. La única diferencia está en quién invoca git commit al final: en modo hook, Git ya tiene el control, así que la herramienta simplemente reescribe el archivo del mensaje; en modo cliente, GitService invoca Git directamente.
flowchart LR
A[git commit] --> B[prepare-commit-msg]
B --> C[dotnet-gitmoji hook]
C --> D[PromptService]
D --> E[GitmojiProvider<br/>cache or embedded default]
D --> F[CommitMessageService]
F --> G[.git/COMMIT_EDITMSG]
H[dotnet-gitmoji commit] --> D
F --> I[GitService<br/>CliWrap -> git]
Características clave
Modo hook. dotnet-gitmoji init --mode shell instala un hook prepare-commit-msg administrado por Husky.Net que intercepta cada git commit. init --mode task-runner apunta a repositorios que ya usan el task runner de Husky.Net, y el hook se salta merge commits, squash merges, amends e interactive rebases para no interrumpir los flujos automatizados.
Modo cliente. dotnet-gitmoji commit actúa como reemplazo directo de git commit, y también admite --title, --scope y --message cuando quieres saltarte parte del prompt. Se desactiva cuando el hook ya está instalado, para que el emoji nunca se aplique dos veces.
Puesta en marcha del equipo. La ruta compartida vive en .config/dotnet-tools.json, .husky/ y Directory.Build.targets. En repositorios con archivo de proyecto, normalmente basta con dotnet restore o con abrir el IDE para restaurar las herramientas y volver a establecer core.hooksPath a través de Husky.Net.
Fuzzy search y descubrimiento. dotnet-gitmoji search <keyword> y el picker en vivo comparten el mismo fuzzy matcher, que busca por nombre, shortcode y descripción del emoji. dotnet-gitmoji list es la forma no interactiva de inspeccionar el catálogo completo.
Opciones de configuración. El asistente interactivo config y el .gitmojirc.json a nivel repositorio exponen los mismos controles: salida en emoji o shortcode, prompts opcionales de scope y mensaje, capitalización del título, sugerencias de scope personalizadas, auto-stage, commits firmados y una URL personalizada para el feed de gitmojis.
Cadena de resolución de configuración. La herramienta lee primero .gitmojirc.json desde la raíz del repo (subiendo por los directorios padre), después ~/.dotnet-gitmoji/config.json, y al final los defaults incluidos. Los ajustes de equipo viajan con el repo, y las preferencias personales se quedan en el home.
Comandos operativos. update refresca la lista cacheada de gitmojis, y remove se encarga del desmontaje del hook. Para hooks administrados por Husky.Net, remove imprime los pasos de limpieza en lugar de editar .husky/ silenciosamente.
Paridad entre instalación local y global. La herramienta detecta si fue instalada de forma global o por proyecto, y escribe la invocación correcta (dotnet-gitmoji o dotnet tool run dotnet-gitmoji) dentro del script de hook generado.
Retos técnicos
prepare-commit-msg con stdin redirigido lejos de la terminal, así que los prompts interactivos de Spectre.Console se niegan a dibujarse. Sin un arreglo, el hook falla en el primer commit.En este caso opté por reabrir stdin desde el dispositivo de terminal antes de que cualquier código lea Console.IsInputRedirected. La herramienta hace esto dentro de Program.Main a través del helper TtyConsoleInput.TryReopenStdin(), que es un no-op inocuo cuando stdin ya es un TTY (modo cliente).
public static async Task<int> Main(string[] args)
{
TtyConsoleInput.TryReopenStdin();
// DI container and CliFx application follow
}
Hice que InitCommand detecte el tipo de instalación al momento de generar el hook, y escriba en el script dotnet-gitmoji hook o dotnet tool run dotnet-gitmoji hook. La misma lógica alimenta los modos shell y task-runner de Husky.Net, así que las tres rutas de hook se mantienen consistentes.
// GitService.cs — detects local vs. global tool manifest at hook-generation time
private async Task<bool> IsLocalToolManifestAsync()
{
var repoRoot = await GetRepositoryRootAsync();
var manifestPath = Path.Combine(repoRoot, ".config", "dotnet-tools.json");
if (!File.Exists(manifestPath))
return false;
var json = await File.ReadAllTextAsync(manifestPath);
var node = JsonNode.Parse(json);
var tools = node?["tools"] as JsonObject;
return tools?.ContainsKey("dotnet-gitmoji") ?? false;
}
private async Task<string> BuildShellHookCommandAsync()
{
var isLocal = await IsLocalToolManifestAsync();
var invocation = isLocal ? "dotnet tool run dotnet-gitmoji" : "dotnet-gitmoji";
return $"{invocation} \"$1\" \"$2\"";
}
dotnet-gitmoji commit en el mismo repositorio haría que el emoji se prefijara dos veces. Detectarlo tan tarde, en el momento del commit, es una solución frágil.Desactivé el modo cliente cada vez que se detecta el hook, al inicio de CommitCommand. El mensaje explica por qué y apunta a remove como salida. Una sola herramienta, un solo lugar que escribe el emoji.
// CommitCommand.cs — guard at the top of ExecuteAsync
public async ValueTask ExecuteAsync(IConsole console)
{
if (await _gitService.IsHookInstalledAsync())
{
await console.Error.WriteLineAsync(
"Error: The prepare-commit-msg hook is already configured to use dotnet-gitmoji.\n" +
"Using both hook mode and client mode would apply the emoji twice.\n\n" +
"Either:\n" +
" • Use 'git commit' and let the hook handle it (hook mode)\n" +
" • Remove the hook from .husky/prepare-commit-msg and use 'dotnet-gitmoji commit' (client mode)");
throw new CommandException("Cannot use client mode while hook is installed.", 1);
}
// ... rest of commit flow
}
La misma detección del tool manifest que usa la generación del hook también mantiene este guard alineado con las instalaciones locales, para que la ruta del repo y la personal se comporten igual.
gitmoji.dev/api/gitmojis. Pegarle a la red en cada commit sería lento y frágil, pero enviar una lista vieja castiga a los equipos que quieren los emojis más nuevos.gitmojis.default.json como recurso para usarlo como valor predeterminado en modo offline, y expuse dotnet-gitmoji update para actualizar una copia en caché en ~/.dotnet-gitmoji/. GitmojiProvider primero lee la caché, recurre al recurso incrustado por defecto y solo hace una petición HTTP durante update.Lo que este proyecto demuestra
| Área | Evidencia |
|---|---|
| Empaquetado de .NET 10 tool | PackAsTool, ToolCommandName y PackageId en DotnetGitmoji.csproj. Se publica en NuGet como dotnet-gitmoji y funciona como herramienta local o global. |
| Arquitectura de CLI | Comandos de CliFx resueltos a través de DI, con separación limpia entre Commands/, Services/, Validators/ y Models/. |
| Interop con Git | CliWrap envuelve cada llamada a git, la herramienta escribe scripts de hook prepare-commit-msg, y se integra con los modos shell y task-runner de Husky.Net respetando instalaciones locales y globales. |
| Puesta en marcha del equipo | Versionar .config/dotnet-tools.json, .husky/ y Directory.Build.targets permite que el hook se recupere durante el restore o al abrir el IDE, en vez de depender de pasos manuales en cada máquina. |
| UX de terminal | Prompts de selección con Spectre.Console, fuzzy search por nombre y shortcode, prompts opcionales de scope y mensaje, y flags de modo cliente para prellenar datos del commit. |
| Testabilidad | Servicios interface-first, tests unitarios con xUnit, NSubstitute para dobles y una ToolIntegrationFixture para cobertura de punta a punta. |
| Capas de configuración | .gitmojirc.json del repo, config global personal bajo ~/.dotnet-gitmoji/ y defaults incluidos, resueltos en ese orden, con config como asistente para la ruta personal. |
Resultados
| Resultado | Evidencia |
|---|---|
| Publicada en NuGet | La herramienta se publica en NuGet como dotnet-gitmoji, lo que mantiene la instalación y las actualizaciones dentro de la cadena estándar de herramientas de .NET. |
| Sin dependencia de Node.js | Un repositorio de .NET que quiere la convención de gitmoji ya no tiene que aprovisionar Node en cada máquina. El .NET SDK es más que suficiente. |
| Puesta en marcha amigable para equipos | Los repositorios pueden versionar el tool manifest, el hook de Husky.Net y el target de restore para que sus integrantes recuperen el prompt a través del restore o de la apertura del IDE, en lugar de configurarlo a mano máquina por máquina. |
| Dos caminos de adopción | Los equipos pueden forzar el hook prepare-commit-msg, mientras que una persona también puede usar dotnet-gitmoji commit con flags cuando un hook a nivel repositorio sería demasiado pesado. |
| La configuración viaja con el repositorio | .gitmojirc.json en la raíz se comparte por Git. Las preferencias personales se quedan bajo ~/.dotnet-gitmoji/, y el asistente interactivo usa la misma estructura de configuración. |
El resultado es simple: un equipo de .NET puede adoptar la convención de gitmoji sin salir de NuGet y MSBuild, y la misma herramienta sigue funcionando como un CLI personal más liviano cuando un hook a nivel repositorio sería demasiado.
Enlaces
- Repositorio: dotnet-gitmoji ➡️
- Paquete en NuGet: dotnet-gitmoji ➡️
- Convención de gitmoji: gitmoji.dev ➡️
- Husky.Net: Husky.Net ➡️
Foto por Tim Witzdam ➡️ en Unsplash ➡️

