1 pontos por GN⁺ 2 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 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 C2 e C2’ 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/bugfix de forma confiável a partir de trunk
      • Como não existe uma referência para frente de trunk para wp/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 code em cima de Fix key entry race e, depois de fazer fetch das atualizações de trunk, precisa fazer rebase mantendo a pilha
    • Como o Git não conhece commits sucessores, não é fácil ver Refactor key entry code a partir de Fix 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
  • 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-feature em cima de bugfix, mesmo sem dependência real, e seguir com a review
    • Durante o desenvolvimento, usar new-feature rebaseada em bugfix e depois desfazer o rebase antes de submeter a branch
  • 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

 
GN⁺ 2 시간 전
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

    • Você está longe de ser o único
      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

    • Acho meio engraçado o autor dizer que o modelo de dados do Git é muito bom e, de passagem, comentar que o fluxo de trabalho com branches do Git não é bom porque elas são só ponteiros para o fim do branch
      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

    • Se você usar jj new A B, o commit da working copy pode ter vários pais, então ele funciona como um commit de merge
      Assim, 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

    • Fico curioso se você prefere Git porque não passou pelas situações que o autor diz serem difíceis no Git e já está acostumado com ele, ou se, mesmo passando por essas situações, ainda acha que o Git oferece um fluxo de trabalho melhor que o Jujutsu
    • Se você só lembrar que precisa executar jj new, dá para misturar git e jj
      O Git sempre aponta para o commit pai, e o jj commit atual passa a parecer mudanças não commitadas na working tree
      Foi assim que eu aprendi jj. Eu usava jj para as coisas em que ele é bom, como lidar com rebase ou mover árvores, e continuava usando comandos git nas tarefas do dia a dia em que eu ainda não sabia o comando equivalente em jj, ou quando algo como git blame vinha primeiro à cabeça
      Na prática, eu não entendi tão bem por que o jj era 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 jj também tem desvantagens. Se o .gitignore mais recente não estiver presente, arquivos binários podem acabar entrando num commit por engano. Felizmente o jj avisa quando você tenta adicionar arquivos muito grandes, mas arquivos pequenos podem passar
      Se 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 jj e acabar testando um commit anterior ao commit que atualizou o .gitignore. Talvez a busca binária devesse ter um modo somente leitura