1

O Jujutsu não tem o passo de "criar um branch", e foi isso que finalmente me fez entender os branches do Git

cover image

O Jujutsu não tem branches?

Quando comecei a usar o Jujutsu depois de anos seguindo os fluxos de trabalho do Git, a primeira coisa que me deixou perdida foi que não havia nenhum branch ao qual o meu trabalho atual pertencesse. Todo mundo que começa no Git aprende o ritual: antes de fazer qualquer trabalho de verdade, crie um branch. Mesmo num projeto pessoal em que você pula essa etapa, pelo menos você está no branch main.

Só que a documentação oficial e um tutorial bastante conhecido já começam direto no trabalho. Ninguém cria um branch nem troca de branch. Na verdade, por mais que eu procurasse, não encontrava no Jujutsu nada chamado "branch" que desse para manipular. No lugar disso existe uma coisa chamada bookmark, que é mapeado para um branch quando você trabalha com o Git. E, mesmo assim, um bookmark não é a mesma coisa que um branch. Eu não estava entendendo nada.

Mas, depois de fazer push do meu trabalho no Jujutsu e abrir PRs com ele algumas vezes seguindo os guias, me ocorreu uma coisa: talvez o Git e o Jujutsu simplesmente queiram dizer coisas diferentes com a palavra "branch". Com isso em mente, reli o capítulo sobre branches do Pro Git1 e a página sobre bookmarks da documentação do Jujutsu, e finalmente a ficha caiu. Um branch do Git não é um "galho" no sentido literal, e faz todo o sentido o Jujutsu chamar o equivalente dele de "bookmark".

O que "um branch do Git é um ponteiro" quer dizer de verdade

Afinal, o que é exatamente um branch do Git? A seção 3.1 do livro Pro Git descreve o branching assim:

Branching (ramificação) significa que você diverge da linha principal de desenvolvimento e continua a fazer o trabalho sem mexer nessa linha principal.

Lendo um pouco mais adiante, você encontra isto:

Um branch no Git é simplesmente um ponteiro móvel e leve para um desses commits.
[...]
O que acontece quando você cria um novo branch? Bem, fazer isso cria um novo ponteiro para você mover por aí.

"Um branch é um ponteiro." Se você usa Git há algum tempo, com certeza já ouviu essa frase. Confesso, meio sem graça, que ela nunca me disse muita coisa, e por anos usei branches no feeling. Hoje entendo o porquê. A explicação se apoia em várias premissas que ninguém menciona, e, para quem teve o Git como primeiro VCS, ela não diz muito. A documentação oficial do Git e os livros clássicos foram escritos por veteranos que conheciam bem outros sistemas de controle de versões, e é fácil eles não perceberem que quem começou pelo Git não compartilha essas premissas.

Então, o que é preciso saber para que "um branch é um ponteiro" faça sentido? A primeira coisa é que, no Git, um branch não é a bifurcação do histórico em si. Para separar as duas coisas, o Pro Git usa expressões como "histórico divergente" (divergent history) para se referir à bifurcação propriamente dita2.

A maioria dos VCSs distribuídos representa o histórico como um DAG3, e não como uma árvore, então as bifurcações e os merges são expressos inteiramente dentro do grafo. O que muda de um VCS para outro é o mecanismo que ele oferece ao usuário para visualizar e manipular essas bifurcações, e a maioria chama esse mecanismo de "branch". No Mercurial, que surgiu mais ou menos na mesma época que o Git, um branch é um metadado gravado de forma permanente em cada commit de uma linha do histórico. Já o Git usa um ponteiro para cada linha do histórico, e cada um aponta para um único commit.

Ainda não ficou claro? Então vamos para a segunda premissa. O que diferencia o Git de outros VCSs é que, onde quer que você esteja, ele sempre te empurra automaticamente para a ponta do histórico. Nos VCSs anteriores, criar branches e alternar entre eles eram operações pesadas, que atrasavam bastante projetos grandes como o kernel do Linux. Torná-las extremamente rápidas foi um dos objetivos centrais quando o Git foi projetado. A solução foi transformar o branch num ponteiro móvel para um único commit, guardado num arquivo de texto de apenas algumas dezenas de bytes.

Para isso funcionar, o Git espera que você edite o código na linha de trabalho para a qual algum branch aponta, e o ponteiro do branch avança automaticamente a cada novo commit. Trocar de branch significa fazer o HEAD, o ponteiro que indica onde você está, coincidir com esse branch. Por isso, no Git, para onde quer que você vá, acaba sempre empurrado para a ponta do histórico.

Como o Git era o único VCS distribuído que eu conhecia, achava que todos funcionavam assim. Descobrir que esse comportamento é específico do Git, e entender o mecanismo por trás dele, foi o que finalmente me fez entender o que significa dizer que um branch do Git é um ponteiro.

O que acontece com uma bifurcação do histórico sem branch?

Então, o Git pode ter uma bifurcação do histórico que não seja um branch? Criar uma é bem fácil: faça checkout de um commit antigo pelo hash.

$ git checkout 9effb1cc737824d9c14e1a8ecbb725e44ccae104
Note: switching to '9effb1cc737824d9c14e1a8ecbb725e44ccae104'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
⋮
HEAD is now at 9effb1c create b.txt

$ echo '444' > d.txt
$ git add .
$ git commit -m 'create d.txt'
[detached HEAD 46b43c3] create d.txt
 1 file changed, 1 insertion(+)
 create mode 100644 d.txt

O Git faz questão de avisar que agora você está em estado de detached HEAD (HEAD desanexado). Normalmente o HEAD aponta para algum branch. Quando ele aponta diretamente para o hash de um commit específico, isso se chama detached HEAD. Empilhar commits nesse estado é considerado arriscado e é melhor evitar, porque esse histórico acaba assim:

  1. Você não consegue encontrá-lo depois
  2. Ele é tratado como lixo e acaba sendo apagado
  3. Você não consegue compartilhá-lo com ninguém

No Git, os commits só passam a ser rastreáveis por pessoas quando pertencem a um branch ou a uma tag. Os commits feitos em detached HEAD perdem a referência assim que você troca de branch, e não aparecem nem no git log --all. O reflog é a única forma de recuperá-los. Além disso, o sistema os trata como histórico desnecessário e eles ficam sujeitos à coleta de lixo, então, quando o prazo expira, somem de vez. Commits que você mesmo pode perder de vista com facilidade, e que podem sumir a qualquer momento, naturalmente são difíceis de compartilhar com outras pessoas.

Por que o Jujutsu tem bookmarks em vez de branches

Como comentei no começo, o Jujutsu não tem nada chamado "branch" que dê para manipular. Mesmo assim, a palavra "branch" aparece o tempo todo na documentação dele, no sentido literal de bifurcação do histórico. O Jujutsu não tem o conceito de "branch atual" nem nenhum mecanismo próprio para identificar bifurcações do histórico, então "branch" é usado no seu sentido original e literal.

Então, por que o Jujutsu não tem branches como os do Git e do Mercurial? No Git, fazer amend ou rebase de um commit gera um hash completamente novo. Hashes que mudam o tempo todo são difíceis de acompanhar, tanto para o sistema quanto para as pessoas, então um commit precisa pertencer a um branch: um rótulo nomeado e estável que diz em qual linha de trabalho ele está. No Jujutsu, por outro lado, a unidade básica do histórico, o change, tem um change ID imutável, separado do commit ID, que muda sempre que o change é revisado. Não importa quantas vezes você reescreva o conteúdo ou faça rebase, dá para acompanhar um change pelo mesmo ID, então quase não há necessidade de rotular cada linha de trabalho.

Além disso, as bifurcações do histórico sempre aparecem no log como um grafo em arte ASCII. Entre o grafo que você vê no log e as descrições dos changes que formam os nós dele, dá para ver de cara a qual linha de trabalho cada change pertence. E qualquer change que tenha descrição ou que modifique arquivos fica no histórico até você descartá-lo explicitamente.

Log do Jujutsu

Como os branches não têm nome, o normal no Jujutsu é trabalhar no equivalente ao detached HEAD do Git. O Jujutsu chama isso de branch anônimo (anonymous branch). O detached HEAD é considerado perigoso, mas os branches anônimos do Jujutsu não têm os dois primeiros problemas que ele tem no Git: "1. Você não consegue encontrá-lo depois" e "2. Ele é tratado como lixo e acaba sendo apagado". E o "3. Você não consegue compartilhá-lo com ninguém"? É para isso que o Jujutsu oferece os bookmarks.

Um bookmark é um ponteiro nomeado associado a um change. Tanto os bookmarks quanto os branches do Git são ponteiros nomeados, mas, diferente de um branch do Git, um bookmark é apenas uma marcação para compartilhar com outras pessoas ou para você não esquecer de algo, sem relação direta com as bifurcações do histórico. É por isso que você pode colocar vários bookmarks na mesma linha de trabalho, e também por isso que, quando você cria novos changes sobre esse change, um bookmark não avança sozinho como um branch do Git.

O Jujutsu não vê necessidade de dar nome às bifurcações do histórico, nem de apontar uma linha de trabalho divergente inteira só para compartilhar histórico com outras pessoas. É por isso que o Jujutsu tem bookmarks em vez de branches.

Branches anônimos + bookmarks: até onde a evolução dos VCSs chegou

No Git, tanto criar bifurcações do histórico quanto fazer merge delas acontece, basicamente, no nível do branch. E como o Jujutsu, que não tem branches, lida com isso?

No Jujutsu, o comando que cria um change novo, a unidade básica do histórico, é o jj new. Como criar uma bifurcação do histórico? Passe para o jj new o ID de um change que já tem um filho. Como fazer merge de um histórico que divergiu? Passe para o jj new os IDs dos changes que você quer juntar. No Jujutsu, estender o histórico, bifurcá-lo e fazer merge funcionam no nível do change, e, surpreendentemente, tudo é a mesma operação básica: criar um change novo.

Você segue trabalhando assim até um ponto em que faz sentido parar. Hora de abrir um PR. É só agora que você precisa de fato dar um nome a essa linha de trabalho4. Coloque um bookmark num change e faça push, e toda a sequência de changes, desde o ponto em que ela saiu da main até esse change, é registrada como um branch no repositório Git remoto, com cada change convertido num commit do Git.

O que você ganha quando a unidade de compartilhamento não está presa a branches? Primeiro, flexibilidade: você pode começar a trabalhar sem decidir um nome nem o que vai compartilhar, e resolver essas questões só quando chegar a hora de compartilhar. Segundo, fica fácil montar uma série de PRs dependentes, algo que ficou bem mais valioso nos últimos tempos. Os stacked pull requests do GitHub, que permitem dividir um PR em unidades pequenas com relações de pai e filho, finalmente ficaram disponíveis para todos em 6 de outubro de 20265, e os bookmarks do Jujutsu deixam muito fácil trabalhar com eles. Para montar stacked PRs com o Git, você cria o primeiro branch com gh stack init, cria um branch filho com gh stack add toda vez que termina uma parte do trabalho e, no fim, faz push e abre os PRs com gh stack submit. É um processo bem trabalhoso e muito dependente de ferramentas específicas. Com o Jujutsu, você coloca bookmarks em ordem na sua linha de trabalho, faz push e roda algo como gh stack link <bookmark1> <bookmark2> <bookmark3>. Só isso. É tão simples que chega a ser anticlimático.

Sem branches, o Jujutsu repensou as operações que o Git tinha construído em torno deles, para que você possa trabalhar em unidades menores e mais flexíveis. Depois de conviver um tempo com um Jujutsu sem branches, finalmente entendi o que são os branches do Git, depois de anos usando-os com uma ideia nebulosa. E aí os conceitos do Jujutsu, tão diferentes dos do Git, começaram a me parecer surpreendentemente racionais. Essa sensação fica ainda mais forte quando você olha a história dos VCSs com certo distanciamento.

Comentei antes que o Mercurial tem um tipo de branch diferente do Git. Mas, como os branches do Mercurial são dados permanentes e globais gravados em cada commit, eles não combinam bem com o desenvolvimento de funcionalidades ou correções de bugs que terminam em poucos dias. Por isso os bookmarks foram adicionados depois, para o trabalho mais leve: marcadores do tipo ponteiro que serviam para repassar o trabalho no push ou para marcar onde você estava trabalhando. Inspirados nos branches do Git, mas independentes dos branches do Mercurial e pensados para o compartilhamento. É daí que vêm os bookmarks do Jujutsu.

O Jujutsu vai um passo além: considera os branches desnecessários para as bifurcações do histórico e simplesmente não os tem. Curiosamente, o Sapling, que a Meta criou a partir de um fork do Mercurial e desenvolveu por conta própria, também não tem branches, mas tem bookmarks. Dá para dizer que "branches anônimos + bookmarks" é um dos pontos a que a evolução do controle de versões chegou, do Git ao Jujutsu, passando pelo Mercurial.

Uma última observação: este post é baseado nas seções "2-4-2. A diferença entre branch e bookmark" e "2-4-3. Trabalhando em branches anônimos" do meu livro, Juju-chu! — Comece seu fluxo de trabalho Jujutsu × IA com `jj new`.

Os primeiros capítulos do livro se concentram na diferença entre os modelos mentais do Git e do Jujutsu. Além de branches versus bookmarks, eles explicam com cuidado os outros pontos em que quem está começando costuma tropeçar. Se este post te deu vontade de experimentar o Jujutsu, ou de dar mais uma chance a ele se da primeira vez não engrenou, dê uma olhada.


  • Este texto foi publicado originalmente em Juju-chu! Blog, o site do meu livro sobre o Jujutsu.

Notas de rodapé

  1. Livro de código aberto sobre Git, escrito por Scott Chacon e Ben Straub, que também é usado como documentação oficial do Git. Ele pode ser lido gratuitamente online. https://git-scm.com/book/pt-br/v2 ↩

  2. https://git-scm.com/book/pt-br/v2/Ramifica%c3%a7%c3%a3o-Branching-no-Git-Branches-em-Poucas-Palavras#divergent_history ↩

  3. Sigla em inglês de Directed Acyclic Graph (grafo acíclico dirigido): uma estrutura de grafo em que os nós, ligados por arestas com direção, nunca formam um ciclo. ↩

  4. Se você prefere dar um nome logo de início, claro que também dá. Crie um bookmark com jj bookmark create ao começar o trabalho e, antes de fazer push, mova-o para o change desejado com jj bookmark move ou jj bookmark advance. ↩

  5. Stacked pull requests generally available - GitHub Changelog ↩

Carregando publicação patrocinada...