DEV Community

Cover image for Estoy construyendo un cliente de Git nativo para macOS en SwiftUI — así va la semana 1
Pool Camacho
Pool Camacho

Posted on

Estoy construyendo un cliente de Git nativo para macOS en SwiftUI — así va la semana 1

Maple es un cliente de Git nativo, gratis y rápido para macOS hecho en SwiftUI. Sin webviews, sin Electron, sin suscripciones. Habla directo con git en tu máquina y no pretende esconder el modelo real de Git detrás de abstracciones raras.

Repo: https://github.com/poolcamacho/Maple · Licencia: MIT · 🇺🇸 Read in English

Por qué otro cliente de Git

En macOS ya hay GUIs de Git muy decentes — Tower y Fork son las que más me gustan. Empecé Maple por algo más específico: quería una app que fuera gratis, open source, 100% SwiftUI nativo, y sin miedo a exponer el modelo real de Git para power users, y a la vez quería aprender SwiftUI moderno construyendo algo que yo mismo usara todos los días.

Maple arrancó con un brief bien acotado:

  • 100% SwiftUI nativo — sin webviews, sin Electron, abre tan rápido como Terminal.
  • Gratis y MIT — nunca suscripción.
  • Muestra Git como es — commit graph con topología real, branches y merges visibles, vista de conflictos de verdad. Cero wizards que escondan lo que está pasando.
  • Respeta tu flujo — shortcuts y acciones para lo que un power user realmente usa: interactive staging, stash, rebase, merge, cherry-pick.

No pretende reemplazar a Tower para todo el mundo. Es el cliente que yo quería que existiera.

Commit graph de Maple con topología real: lanes con colores y edges curvos por cada parent

Lo que ya hace

Esto es un post de avance, no un lanzamiento. Las capturas las subiré en el próximo post. Pero esto ya funciona de punta a punta en repos reales (llevo varios días dogfooding con la misma app para commitear su propio código):

  • Abrir cualquier repo local con el folder picker y validación de .git
  • Sidebar con lista de repos, branches locales y remotos
  • Toolbar con Pull, Push, Fetch, Stash, Branch, Merge, Rebase
  • Cuatro tabs: Changes, History, Branches, Stashes
  • Diff viewer con hunks coloreados, números de línea, y un toggle de Blame
  • Commit graph con topología real — algoritmo de lanes, edges curvos por cada parent, merges dibujados con círculos anillados
  • Merge y rebase con manejo de conflictos — detecta UU/AA/DD, banner de operación con Abort / Continue / Skip, y resolución por archivo con Use Ours / Use Theirs
  • Auto-refresh vía FSEvents sobre .git/
  • Layout adaptativo de desktops anchos a pantallas compactas

Tab Branches de Maple con branches locales y remotos, acciones de checkout, create y delete

La arquitectura en una pantalla

Models/     — Data pura y Sendable (AppState, GitModels, StashModels)
Services/   — GitService (actor, ejecuta el CLI)
              GitCoordinator (@MainActor, orquestación)
              Extensiones por comando (GitCommands, GitBranchOps,
              GitStashOps, GitMergeRebase)
              CommitGraphBuilder, ConflictParser, FileWatcher
Views/      — Un archivo por vista, cero lógica de negocio
Utils/      — FolderPicker, DateExtensions
Enter fullscreen mode Exit fullscreen mode

Tres decisiones que vale la pena llamar aparte:

GitService es un actor, así que todas las llamadas al CLI se serializan por un solo "portero". GitCoordinator es @MainActor y hace de pegamento entre la UI y el actor — las vistas solo llaman state.coordinator.* y nunca tocan un Process directamente. Resultado: lógica de negocio cero en las vistas, fácil de testear y refactorizar.

Cada invocación de git abre un /dev/null fresco para stdin y cierra sus pipes explícitamente. Sin eso me salía NSPOSIXErrorDomain code=9 / EBADF después de comandos largos como push, porque FileHandle.nullDevice es un singleton compartido que termina en estados raros después de muchos posix_spawn. Abrir /dev/null por llamada con closeOnDealloc: true lo arregla de tajo.

El commit graph no es el "punto + línea vertical" de toda la vida. Construye un layout real de lanes: para cada commit, reclama el lane que lo estaba esperando (el vínculo child→parent); si no hay, reusa un lane libre. Los primeros parents se quedan en el mismo lane para que la línea principal quede recta; los parents adicionales de merges abren lanes laterales con edges curvos. Los edges se resuelven en una segunda pasada porque un parent puede aparecer muchas filas más abajo.

El UX de conflictos es por archivo, no por hunk — a propósito para v1. Cuando cae un merge con conflicto, el tab Changes marca cada archivo conflictivo con un ! morado, y cada uno tiene tres botones: Use Ours, Use Theirs, o editas los markers tú mismo en tu editor favorito. Arriba aparece un banner persistente que dice Merging X con Abort / Continue / Skip. Sin modales, sin secuestrarte el flujo.

Vista de resolución de conflictos en Maple con el banner Merging, los markers resaltados y los botones Use Ours / Use Theirs por archivo

Lo que ya está armado alrededor del código

Porque la idea es que esto crezca como proyecto OSS serio, no como experimento de fin de semana:

  • GitHub Actions CI — build + xcodebuild analyze + SwiftLint strict en cada push y PR
  • CodeQL — análisis semanal de Swift más en cada PR que toque código Swift
  • Workflow de release — taggeas v* y sale un .app sin firmar zipeado automáticamente
  • Branch protection en master — status checks requeridos, no force push, no delete, conversations resueltas
  • Dependabot para GitHub Actions, así las versiones no se quedan oxidadas
  • SECURITY.md con private vulnerability reporting activado
  • Issue forms y PR template que fuerza la regla de "ni una sola línea de lógica de negocio en las vistas"

Nada de esto es heroico. Es el andamiaje aburrido que separa un hobby project de un repo donde la gente realmente puede contribuir.

Lo que sigue

El roadmap que estoy atacando en orden:

  • [ ] Interactive staging — stage de hunks o líneas individuales, no solo archivos enteros
  • [ ] Tag management — crear, listar, borrar tags desde la UI
  • [ ] Search / filtering — filtrar commits y archivos con fuzzy match
  • [ ] Clone from URL — ahora solo abres repos existentes, clonar es el paso obvio
  • [ ] Remote management — agregar, quitar, configurar remotes sin CLI
  • [ ] Keyboard shortcutsCmd+S stage, Cmd+Enter commit, Cmd+K command palette
  • [ ] Persistir repos abiertos entre sesiones
  • [ ] Settings & preferences
  • [ ] Releases firmados + notarizados cuando la app ya esté lista para usuarios no-devs

Pruébalo y ayúdame a darle forma

MIT, open source, todo en público:

github.com/poolcamacho/Maple

Si tienes macOS 14+ y Xcode 16+, git clone + Cmd+R y lo tienes corriendo en menos de un minuto. Abre issues si te topas con un flujo de Git que la UI vuelve torpe — prefiero arreglar eso a adivinar qué necesitan los power users.

Si quieres patrocinar el proyecto para que se mantenga gratis y activo, hay botón Sponsor arriba del repo, o directo en github.com/sponsors/poolcamacho. El patrocinio mantiene Maple libre para todos y me ayuda a cubrir el presupuesto de firma + notarización que voy a necesitar para shipear a usuarios no-devs.

El próximo post probablemente sea sobre interactive staging — es el problema de diseño más interesante de la lista. Nos leemos.

Top comments (0)