Git não está bem
(billjings.com)- O Git teve sucesso como repositório distribuído de código-fonte, mas o tratamento de workflow distribuído está mais para uma solução acoplada depois, e seus limites ficam aparentes
- Commits e branches do Git não conseguem expressar por si mesmos commits sucessores, histórico de amend, histórico de rebase nem estado descartado
- Em Stacked PRs, é preciso encontrar PRs subsequentes e fazer rebase mantendo a pilha, mas o Git tem dificuldade para identificar essa relação de forma confiável
- O Git mantém estados mutáveis como staging, unstaged, sistema de arquivos e HEAD fora de commits e branches, o que torna o aprendizado e o uso mais complexos
- No fluxo de desenvolvimento assíncrono, em que vários PRs precisam ser usados juntos antes do merge, o modelo de histórico imutável voltado para trás do Git gera problemas repetidos
Os dois papéis do Git
- O Git é ao mesmo tempo um repositório distribuído de código-fonte e uma ferramenta de workflow distribuído
- Como repositório de código, ele teve muito sucesso, mas a forma como lida com workflow distribuído se parece, em grande parte, com uma solução acrescentada depois
- Desenvolvimento assíncrono é quase uma condição básica, como descreve o East River Source Control, e isso acontece não só em colaboração entre fusos horários diferentes, mas também quando alguém trabalha com defasagem temporal em relação a si mesmo
- O jj é uma ferramenta que expõe com mais clareza as limitações do Git, e quem sente que o Git já basta provavelmente não vai experimentar o jj com seriedade
Relações que o modelo básico do Git deixa escapar
- No centro da forma de pensar do Git estão commits e branches
- Commits são objetos imutáveis que contêm código-fonte e histórico
- Branches são ponteiros mutáveis com um log associado
- Diagramas típicos de Git desenham commits como
C1,C2,C3, de modo que ordem e relação parecem claras, mas os nomes reais dos commits em um repositório se parecem mais com hashes ou mensagens, então essa relação de ordem não existe dentro do sistema - Notações como
C2eC2’após um rebase são apenas explicações fáceis para humanos entenderem; o Git não sabe que esses dois commits correspondem um ao outro - Para encontrar commits sucessores de um commit específico, é preciso vasculhar todas as branches e localizar os commits no caminho que leva àquele commit, então isso não é algo simples
No Git não existe o “C”
- Um commit do Git não consegue saber por si só as seguintes informações
-
Commits sucessores
- O histórico de modificações que liga um commit antigo a um novo commit após um amend
-
Histórico de rebase
- Se aquele commit foi descartado ou não
- Branches também têm limitações
- Branches têm um conceito de histórico, mas é difícil confiar que correspondam 1:1 a mudanças de código
- Branches não têm relação entre si; por exemplo, não dá para localizar
wp/bugfixde forma confiável a partir detrunk - Como não existe uma referência para frente de
trunkparawp/bugfix, isso também não é uma relação alcançável - Diagramas de Git parecem mostrar ordem e correspondência para quem olha, mas podem exagerar as capacidades reais oferecidas pela ferramenta
-
Por que Stacked PR é difícil
- Ao colaborar com pessoas em outros fusos e evitar merge antes da review, é preciso pipelinear o trabalho, como uma CPU
- Em vez de criar um PR e esperar a review terminar, cria-se um segundo PR em cima do primeiro e depois outro em cima desse, colocando vários PRs sequenciais em review ao mesmo tempo; esse modelo é o Stacked PR
- O Git dificulta lidar com a estrutura de Stacked PR de forma confiável
- Você cria um PR subsequente como
Refactor key entry codeem cima deFix key entry racee, depois de fazer fetch das atualizações detrunk, precisa fazer rebase mantendo a pilha - Como o Git não conhece commits sucessores, não é fácil ver
Refactor key entry codea partir deFix key entry race - O commit pode até ter sido descartado, então, mesmo que você consiga ver um commit sucessor, é difícil saber se ele ainda é o estado mais recente
- Branches são usadas quase como se fossem o próprio PR, mas nesse fluxo é fácil sobrescrevê-las por engano
- Você cria um PR subsequente como
- Ferramentas de stacking como Graphite conseguem fazer isso sobre o Git, mas não conseguem reforçar os próprios commits ou branches do Git
- É preciso criar um repositório separado de metadados de branches e sincronizá-lo com o Git
- Se o usuário manipular o próprio Git diretamente, esse repositório pode ficar dessincronizado em relação ao estado do Git
O estado mutável fica fora dos commits
- Vários problemas do Git decorrem da forma como ele não modela diretamente a mutabilidade
- No workflow de edição do Git, existem estados separados fora de commits e branches
- Staging, ou index, é um snapshot do código-fonte criado a partir da cópia de trabalho, e novos commits são criados a partir dele
- Unstaged é um segundo diff que representa a diferença entre o index e o sistema de arquivos
- O sistema de arquivos contém o que foi feito checkout, e a isso se somam as mudanças staged e unstaged
- HEAD é o ponto em que novos commits são criados
- O stash funciona como um repositório separado para salvar e restaurar mudanças staged e unstaged
- Ao mudar o checkout para outro commit ou branch, o Git tenta alinhar o sistema de arquivos ao novo ponto enquanto preserva os diffs staged ou unstaged
- Embora os comandos sejam diferentes, se olhar apenas para as relações de seta, esse processo se parece com um rebase que move o staging para cima de uma nova base
Por que é difícil modelar tudo como commit
- Staging e a cópia de trabalho também têm ancestrais claros e contêm código-fonte, então, olhando apenas para o estado estático, poderiam ser representados como commits
- Mas o ID de um commit é um hash do conteúdo, então, se o commit fosse mutável, seu ID mudaria o tempo todo
- Para apontar de forma consistente para “o que” são o staging e a cópia de trabalho, seria preciso tratá-los mais como branches do que como commits, mas branches têm as limitações já discutidas
- Essa complexidade leva a problemas reais
- Aprender e usar Git fica mais difícil, porque o mesmo conceito existe em ambos os lados de forma separada
- O estado completo do repositório difere muito do estado obtido por clone, então exportar isso fica estranho
- Fluxos assíncronos em que o conjunto de mudanças muda com o tempo não funcionam bem
- O lado mutável do sistema não consegue expressar merge, então às vezes não consegue representar o workflow real
Quando o Git não consegue expressar o workflow real
- Ao desenvolver em uma nova feature branch sem ainda fazer commit, você pode encontrar um bug no dispositivo que atrapalha o desenvolvimento
- Se esse bug não bloquear a nova feature, mas tornar o desenvolvimento incômodo, você pode guardar o trabalho com stash, mudar para uma nova branch, criar um teste de reprodução e a correção, e abrir um PR
- Depois, ao voltar para a branch da nova feature, as opções ficam limitadas
- Fazer rebase de
new-featureem cima debugfix, mesmo sem dependência real, e seguir com a review - Durante o desenvolvimento, usar
new-featurerebaseada embugfixe depois desfazer o rebase antes de submeter a branch
- Fazer rebase de
- Com Git, não é possível expressar o estado em que “o espaço de trabalho de edição precisa conter ao mesmo tempo todo o código do bugfix e o código já commitado da new feature”
- Essa necessidade aparece com a mesma estrutura em problemas mais difíceis, como testes de compatibilidade com PRs ainda não mergeados
- Com a ferramenta adequada, como Jujutsu megamerges, é possível manter vários PRs em paralelo e ainda usá-los juntos no espaço de edição
Git não é mais suficiente
- As ferramentas de controle de versão do começo dos anos 2000 eram difíceis de usar e administrar, com qualidade irregular, e havia uma percepção ampla de que até o Subversion era doloroso
- Na época, não era comum querer ter uma cópia completa do repositório localmente, nem era universal querer criar branches locais
- Muita gente se incomodava com bloqueio de arquivos, mas algumas pessoas achavam isso necessário e até perguntavam se havia como bloquear arquivos ou diretórios individuais no Git
- Para quem vivia diretamente workflows distribuídos, como no open source, DVCS foi recebido como um curativo que evitava reabrir feridas antigas
- Hoje, para quem usa um workflow distribuído de verdade, o modelo de histórico imutável voltado para trás do Git se torna uma fonte recorrente de problemas
- Empresas como a Meta já usam há quase uma década sistemas internos muito superiores ao Git
- A tendência de “agora o Claude mexe no Git por você” não torna essas alternativas irrelevantes
- Com o uso de LLMs, parece que engenheiros estão fazendo ainda mais desenvolvimento assíncrono do que antes, mesmo dentro de uma única máquina
1 comentários
Opiniões no Lobste.rs
Teria sido bom mostrar como o jj resolve os problemas levantados no texto
Isso pode ser óbvio para usuários de jj, mas provavelmente eles não são o público principal do texto
Pessoalmente, nunca precisei dos recursos citados no texto como evidência de que o Git não está bem
Fico pensando se sou só eu
Um dos pontos importantes sobre ferramentas é que elas fazem parte de um sistema dinâmico. O que uma ferramenta torna possível afeta “o que eu acredito ser capaz de fazer”, e essa crença por sua vez muda a percepção da ferramenta e a direção da sua evolução
Quando uma ferramenta abala o estado atual das coisas, as crenças e expectativas sobre o que é possível fazer também mudam junto
Parece interessante, mas o diagrama me deixa tonto
Sobre a ideia de que a situação não é tão séria quanto no começo dos anos 2000, e que as limitações dos sistemas de controle de versão anteriores ao Git eram bem claras, o Darcs surgiu antes do Git e corrigia de forma fundamental alguns problemas do controle de versão baseado em snapshots
No começo ele perdeu espaço por causa do desempenho ruim, mas depois isso melhorou, e as pessoas não voltaram para conferir. Existem outros sistemas de controle de versão fazendo coisas interessantes, então eu preferiria que isso não fosse tratado como se “se não é Git, então é Jujutsu” fosse a única alternativa. Vejo esse tipo de lógica com frequência demais
Isso também é um problema do modelo de dados
Como o jj lida com isto? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png
jj new A B, o commit da working copy pode ter vários pais, então ele funciona como um commit de mergeAssim, a working copy recebe as mudanças dos dois pais, e você pode continuar trabalhando em cima desse merge ou fazer amend em um dos commits
Ainda prefiro Git, e o autor me parece ter um certo viés
jj new, dá para misturargitejjO Git sempre aponta para o commit pai, e o
jj commitatual passa a parecer mudanças não commitadas na working treeFoi assim que eu aprendi
jj. Eu usavajjpara as coisas em que ele é bom, como lidar com rebase ou mover árvores, e continuava usando comandosgitnas tarefas do dia a dia em que eu ainda não sabia o comando equivalente emjj, ou quando algo comogit blamevinha primeiro à cabeçaNa prática, eu não entendi tão bem por que o
jjera melhor até começar a usá-lo todos os dias; lendo a respeito, eu pensava “será que eu realmente preciso disso?” ou “mas isso eu já consigo fazer com Git”Claro, o
jjtambém tem desvantagens. Se o.gitignoremais recente não estiver presente, arquivos binários podem acabar entrando num commit por engano. Felizmente ojjavisa quando você tenta adicionar arquivos muito grandes, mas arquivos pequenos podem passarSe houver arquivos rastreados ou logs no diretório atual durante o debugging, eles também podem entrar, então é bom revisar toda a diffstat depois de manipular a árvore toda
Isso pode ser um problema especialmente se você estiver fazendo busca binária com
jje acabar testando um commit anterior ao commit que atualizou o.gitignore. Talvez a busca binária devesse ter um modo somente leitura