Cancelei o GitKraken e construí meu próprio cliente Git em dois dias

O GitKraken tava pesado demais pra minha máquina e pro meu bolso
Chegou o e-mail da renovação anual do GitKraken e eu olhei praquilo com um carinho especial: o carinho de quem olha pra conta de luz. Pago todo ano. Por um cliente Git. Que demora uma eternidade pra abrir e engole RAM como se fosse o Chrome com 40 abas.
O pior nem era o dinheiro, era a espera. Clicar no ícone, ir buscar café, voltar, e o bicho ainda carregando. Minha máquina aguenta build de projeto grande o dia inteiro, mas sofria pra renderizar um grafo de commits em Electron.
Aí eu pensei: e se em vez de trocar por outra ferramenta paga (ou gratuita e capada), eu simplesmente construísse a minha? Do meu jeito, enxuta, sem conta anual.
Nasceu o GitCraque. Este post conta como.
Tempo estimado de leitura: 5 min

Dois dias de vibe coding e um trocadilho que eu não vou abandonar
Aí eu construí o meu. Chama GitCraque, e foi basicamente dois dias de vibe coding do zero até virar meu cliente Git do dia a dia. Sem Electron, sem conta anual, sem esperar splash screen.
O nome? Olha o GitKraken: um polvo gigante, oito braços, mora no fundo do mar e nunca ganhou uma Copa. Oito braços e zero troféus é um aproveitamento ruim pra qualquer elenco. Do outro lado tinha um cara com o corte de cabelo mais indefensável da história do futebol, dois gols na final e a alcunha de O Fenômeno. A escolha se fez sozinha. Troquei o K pelo C, o cefalópode pelo camisa 9.
Yes, it's a tribute. Yes, it's a pun. No, we will not stop.
Pra testar é um comando, dentro de qualquer repositório:
npx gitcraque # dentro do diretório do repositório
npx gitcraque ~/code/project # ou aponta um path, tipo `git -C`
npx gitcraque --repo ~ --port 5271
npx gitcraque --no-open # não abre o browser
O repo tá em https://github.com/frederico-kluser/GitCraque. E aviso logo: o projeto tá crescendo, evoluindo, não tá finalizado. Tá jogando bem, mas ainda tá em temporada.
A escalação: um backend com uma dependência só
Time enxuto, sem elenco inchado e sem folha salarial em node_modules. A escalação ficou assim:
- Goleiro: Node puro com
node:http. Segura a API sem framework na frente. Nada de Express, nada de Fastify, só o que vem na caixa. - Zagueiro:
child_process, marcando o binário dogitem cima, sempre com argv em array. Sem shell interpola no meu time. - Lateral:
ws. A única dependência do backend. Uma. Total. - Meio-campo: React 19 + Vite, distribuindo o jogo pra SPA.
- Camisa 10: Tailwind + Motion UI, o primeiro toque e a beleza.
- Ponta:
@dnd-kit/core, arrasta o commit e deixa o marcador pra trás. - Camisa 9: SVG escrito à mão. O grafo inteiro é
<circle>e<path>com Bézier cúbica. Nenhuma lib de gitgraph. O drible é todo nosso.
E o Electron? Não foi convocado. Cliente Git empacotado em Chromium é aquele jogador que chega pesado, come metade da RAM e demora pra aquecer. Aqui não.
Requisitos pra escalar: Node >= 22.13 (o node:sqlite só larga a flag experimental a partir dessa versão) e git no PATH. Chuteira é opcional.
As funcionalidades que me fizeram esquecer o GitKraken
Time escalado, agora o que ele faz em campo. Seis coisas, na ordem em que me conquistaram:
-
O grafo de histórico. O backend roda
git log --all --topo-ordere entrega os dados crus; o front calcula o(X, Y)de cada commit com algoritmo próprio, separando filhos de branch de filhos de merge pra traçar rotas que nunca se cruzam. O desenho é SVG escrito à mão,<circle>pros commits e<path>com Béziers pras branches, com virtualização de janela. Repo com dezenas de milhares de commits e o scroll continua liso. Nenhuma lib de gitgraph envolvida. O drible é nosso. -
Worktrees sem checkout. Clicou num worktree na lateral, o servidor faz
process.chdir()pro path absoluto e avisa por WebSocket. Nenhumgit checkoutacontece, a working tree de ninguém é tocada. É trocar de posição sem pedir substituição pro quarto árbitro. -
Minha favorita, e olha que eu testei todas: deletar worktree, branch local e branch na origin num comando só. No GitKraken eu fazia isso em dois comandos separados, toda vez, e toda vez me irritava. Agora é um clique. Parece bobo, mas é o tipo de atrito diário que acumula até você cancelar uma assinatura.
-
Drag-and-drop com intenção. Arrastar um commit em cima de uma branch propõe cherry-pick; branch em cima de branch abre a escolha entre merge e rebase. O comando cru aparece antes de rodar, e qualquer coisa que reescreve histórico exige press-and-hold: o botão só cede se você segurar. Tempo de pensar antes de chutar.
-
Push que nunca trava. Modelo trampolim com
GIT_ASKPASSapontando pro próprio script do Node, que responde o git via stdout. O token viaja por unix socket com nonce de uso único. Nunca no argv, nunca em disco. -
Picker quando você abre fora de um repo. Tela com recents, varredura das pastas usuais e até
git initpra onde você tá parado. Começou fora de um repositório? Não é erro, é scout.
O squash gráfico sem abrir vim na sua cara
Todo cliente Git que tenta fazer rebase interativo "bonito" cai numa das duas armadilhas: ou emula um terminal inteiro dentro da UI (pesado, frágil, horroroso), ou desiste e abre o vim na sua cara mesmo. Eu odeio as duas opções. O git já sabe fazer rebase interativo sozinho, o problema é que ele insiste em abrir um editor pra você editar o git-rebase-todo na mão.
Aí veio a sacada: e se o "editor" fosse um script meu?
O git respeita a variável GIT_SEQUENCE_EDITOR. Quando ela tá setada, ele gera o git-rebase-todo, chama o programa apontado passando o caminho do arquivo, e espera um exit 0 pra continuar. Eu injeto GIT_SEQUENCE_EDITOR="node proxy-editor.mjs" e o script faz o trabalho sujo:
// server/proxy-editor.mjs
import { readFileSync, writeFileSync } from 'node:fs';
const todoPath = process.argv[2];
const targets = new Set(process.env.GITCRaque_SQUASH.split('\n'));
const lines = readFileSync(todoPath, 'utf8').split('\n');
const edited = lines.map((line, i) => {
const [action, hash] = line.split(' ');
if (action === 'pick' && i > 0 && targets.has(hash)) {
return line.replace(/^pick/, 'squash');
}
return line;
});
writeFileSync(todoPath, edited.join('\n'));
process.exit(0);
Lê o todo que o próprio git gerou, troca pick por squash nas linhas marcadas na UI (mantendo o primeiro commit como pick, senão o rebase inteiro desanda), grava e sai com exit 0. O git aplica o rewrite como se um humano tivesse editado o arquivo com todo cuidado do mundo.
Zero emulação de terminal. Zero vim.
Três commits entram, um sai. E ninguém viu como.
A volta por cima: sobreviver ao Chrome descartando sua aba
O Chrome tem dois jeitos de economizar recurso com aba em segundo plano, e os dois doem diferente.
O primeiro é o freezing: as filas de task param e o WebSocket volta half-open. O readyState jura que tá OPEN, mas a conexão morreu do outro lado. É o jogador em pé no campo, fora da jogada.
O segundo é pior. Discarding: o Chrome apaga a página da memória inteira e ela volta do zero, como se fosse a primeira visita.
O GitCraque cobre os três caminhos de retorno: visibilitychange, resume e pageshow com persisted. E o snapshot do estado da view acontece quando a aba esconde, nunca na saída. Por quê? Porque beforeunload e unload simplesmente não disparam quando o browser descarta a aba. Você programa o handler, confia nele, e ele nunca roda. Descobri do jeito divertido.
Tem ainda um root boundary pra segurar render que explode, e o reload automático tem budget. Loop de reload é pior que tela quebrada: você nem consegue abrir o devtools pra investigar.
Joelho reconstruído, artilharia da Copa. It works.
O que eu aprendi testando tudo com TDD
Me perguntaram qual foi a parte difícil. Honestamente? Nenhuma foi muito complicada, e o motivo é chato de tão simples: eu fui testando com qualidade tudo que era feito. TDD do começo ao fim, mais teste end-to-end por cima. Hoje o npm test roda 472 testes: 319 de server, 51 do grafo, 20 de drag-and-drop, 82 do viewer. E ainda tem 39 checks de e2e fora desse bolo.
Tem um trade-off real aqui, e eu aprendi na marra: roda um comando de teste por vez. A suíte do grafo mede proporção de wall-clock, então se você rodar ela junto com outro processo pesado ela marca uma falta que nunca aconteceu. E não tem VAR aqui.
Segurança foi decisão de projeto, não detalhe: o servidor escuta só em 127.0.0.1, recusa request com Host/Origin de fora, e nunca, jamais, deve ser exposto à rede.
E sejamos claros: o projeto tá crescendo. Não tá finalizado, vai melhorando com o tempo. Igual ao Fenômeno depois do joelho, aliás.
Testa aí e me conta o que quebrou
Chega de ler. Entra num projeto seu e roda:
npx gitcraque
npm install -g gitcraque
cd ~/code/project && gitcraque
A view abre sozinha no navegador, já escalada no repositório. Sem instalar nada permanente, sem cadastro, sem cartão de crédito.
Um aviso antes que você xingue o projeto: npm i github:frederico-kluser/GitCraque não funciona. O pacote publicado já traz a SPA buildada, e o build só roda no npm pack. Do source é clone + npm run build.
Gostou? Estrela no repo, abre issue, manda PR. Ou nem usa: se inspire e construa a sua. E manda esse post no Slack ou no WhatsApp praquela pessoa que ainda paga cliente Git todo ano. Você conhece uma.
Boots optional.