Git Guia Prรกtico
Richard E. Silverman
Novatec
Authorized Portuguese translation of the English edition of titled Git Pocket Guide, ISBN 9781449325862 © 2013 Richard Silverman. This translation is published and sold by permission of O'Reilly Media, Inc., the owner of all rights to publish and sell the same. Tradução em português autorizada da edição em inglês da obra Git Pocket Guide, ISBN 9781449325862 © 2013 Richard Silverman. Esta tradução é publicada e vendida com a permissão da O'Reilly Media, Inc., detentora de todos os direitos para publicação e venda desta obra. © Novatec Editora Ltda. [2013]. Editor: Rubens Prates Tradução: Acauan Fernandes Revisão técnica: Aurelio Jargas Revisão gramatical: Marta Almeida de Sá Editoração eletrônica: Carolina Kuwabata ISBN: 978-85-7522-379-6 Histórico de impressões: Novembro/2013
Primeira edição
Novatec Editora Ltda. Rua Luís Antônio dos Santos 110 02460-000 – São Paulo, SP – Brasil Tel.: +55 11 2959-6529 Fax: +55 11 2950-8869 E-mail: novatec@novatec.com.br Site: www.novatec.com.br Twitter: twitter.com/novateceditora Facebook: facebook.com/novatec LinkedIn: linkedin.com/in/novatec VC20131028
capítulo 1
Compreendendo o Git
Neste capítulo inicial, discutimos como o Git funciona, definindo termos e conceitos importantes que você deve entender para usá-lo com eficácia. Algumas ferramentas e tecnologias se prestam para uma abordagem tipo “caixa-preta”, na qual os usuários novatos não prestam muita atenção a como uma ferramenta funciona sob a interface. Você se concentra primeiro em aprender a usar a ferramenta; os “porquês” e os “como” podem ficar para mais tarde. O projeto específico do Git, entretanto, é mais bem servido pela abordagem oposta, na qual algumas decisões internas de projeto fundamentais se refletem diretamente no modo pelo qual você o usa. Entendendo desde o início e em razoável detalhamento diversos pontos-chave de seu funcionamento, você poderá começar a usar normalmente o Git de forma mais rápida e confiante e ficar mais bem preparado para continuar aprendendo sozinho. Assim, eu o aconselho a despender o tempo necessário para ler este capítulo primeiro, em vez de simplesmente ir diretamente para os capítulos mais práticos e tutoriais que se seguem (a maioria dos quais supõe um conhecimento básico do material apresentado aqui, de qualquer forma). Você provavelmente achará que sua compreensão e seu domínio do Git aumentarão mais facilmente se fizer isso.
Visão geral Começamos apresentando alguns termos e ideias básicos, a noção geral de branch e o mecanismo habitual por meio do qual você compartilha seu trabalho com outros no Git. 16
Capítulo 1 ■ Compreendendo o Git
17
Terminologia Um projeto Git é representado por um “repositório” que contém o histórico completo do projeto, desde a sua concepção. Um repositório, por sua vez, consiste de um conjunto de snapshots do conteúdo do projeto – coleções de arquivos e diretórios – chamados “commits”. Um único commit engloba o seguinte:
Um snapshot do conteúdo do projeto, chamado de “tree” Uma estrutura de arquivos e diretórios aninhados representando um estado completo do projeto
A identificação “author” Nome, endereço de e-mail e timestamp indicando quem fez as mudanças que resultaram neste estado do projeto e quando isso foi feito
A identificação “committer” A mesma informação sobre a pessoa que adicionou este commit no repositório (a qual pode ter sido outra que não o autor)
Uma “mensagem de commit” O texto usado para comentar as mudanças feitas por este commit
Uma lista de zero ou mais “commits pais” Referências a outros commits no mesmo diretório, indicando estados imediatamente anteriores do conteúdo do projeto O conjunto de todos os commits em um repositório, conectados por linhas indicando seus commits pais, forma um todo chamado de “grafo de commits” do repositório, mostrado na figura 1.1.
Figura 1.1 – O “grafo de commits” do repositório.
18
Git Guia Prático
As letras e os números aqui representam commits, e as setas apontam de um commit para seus pais. O commit A não tem pais e é chamado de root commit (“commit raiz”); ele foi o commit inicial no histórico desse repositório. A maioria dos commits tem um único pai, indicando que se desenvolveram diretamente a partir de um único estado anterior do projeto, geralmente incorporando um conjunto de mudanças relacionadas feitas por uma pessoa. Alguns commits, no caso em exame apenas o que tem rótulo E, possuem múltiplos pais e são chamados de “merge commits”. Isso indica que o commit junta as mudanças feitas em branches distintos do grafo de commits, muitas vezes combinando contribuições feitas separadamente por pessoas diferentes. Já que normalmente fica claro a partir do contexto em qual direção o histórico prossegue – geralmente, como aqui, os commits pais aparecem à esquerda dos seus filhos –, omitiremos as setas nesses diagramas daqui para a frente.
Branches Os rótulos no lado direito desta imagem – master, topic e release – denotam branches. O nome do branch se refere ao commit mais recente nele; aqui, os commits F, 4 e Z, respectivamente, são chamados de “ponta” do branch. O próprio branch é definido como o conjunto de todos os commits do grafo que possam ser alcançados a partir da ponta, seguindo as setas dos pais voltando pelo histórico. Aqui, os branches são: • release = {A, B, C, X, Y, Z} • master = {A, B, C, D, E, F, 1, 2} • topic = {A, B, 1, 2, 3, 4} Observe que branches podem se sobrepor; aqui, os commits 1 e 2 estão tanto no branch master quanto no topic, e os commits A e B estão nos três branches. Geralmente, você está “em” um branch, examinando o conteúdo correspondente ao commit da ponta desse branch. Quando você altera alguns arquivos e adiciona um novo commit contendo as mudanças (chamado “execução de commit no repositório”), o nome do branch avança para o próximo commit, o qual, por sua vez, aponta para o commit antigo como o único pai; essa é a forma pela qual branches
Capítulo 1 ■ Compreendendo o Git
19
se movem para a frente. De tempos em tempos, você fará com que o Git execute uma fusão (merge) de diversos branches (mais frequentemente dois, mas pode ser mais), juntando-os como no commit E da figura 1.1. Os mesmos branches podem sofrer fusão repetidamente ao longo do tempo, mostrando que continuaram a progredir separadamente enquanto você periodicamente combinava seus conteúdos. O primeiro branch em um novo repositório é chamado de master por padrão, e é costumeiro usar esse nome se houver apenas um branch no repositório, ou para o branch que contiver a linha principal de desenvolvimento (se isso fizer sentido para o seu projeto). Você não precisa fazer isso, porém, e não há nada especial no nome “master” além da convenção e de seu uso como um default por alguns comandos.
Compartilhando trabalho Há dois contextos nos quais o controle de versões é útil: privado e público. Ao trabalhar sozinho, é útil executar commits “logo e com frequência”, de modo que você possa explorar diferentes ideias e fazer mudanças livremente sem se preocupar com a recuperação do trabalho anterior. Esses commits provavelmente são um pouco desorganizados e têm mensagens de commit enigmáticas, o que é bom, porque eles precisam ser inteligíveis apenas para você e por um período curto de tempo. Assim que uma parte do seu trabalho estiver terminada e você estiver pronto para compartilhá-la com outros, porém, talvez queira reorganizar esses commits, para torná-los bem projetados com relação à reusabilidade das mudanças sendo feitas (especialmente com software) e para dar a eles mensagens de commit bem escritas e significativas. Em sistemas centralizados de controle de versão, o ato de executar commit em uma mudança e publicá-la para que outros a vejam é o mesmo: a unidade de publicação é commit, e executar um commit requer publicar (aplicar a mudança no repositório central onde os outros possam vê-la imediatamente). Isso dificulta o uso do controle de versão tanto em contextos privados quanto em públicos. Separando o commit e a publicação, e dando a você ferramentas com as quais editar e reorganizar commits existentes, o Git incentiva um uso melhor do controle de versão.
20
Git Guia Prático
Com o Git, compartilhar trabalho entre repositórios ocorre por modo de operações chamadas “push” e “pull”: você traz (pull) mudanças de um repositório remoto e envia (push) mudanças para ele. Para trabalhar em um projeto, você o “clona” a partir de um repositório existente, possivelmente em uma rede por meio de protocolos como HTTP e SSH. Seu clone é uma cópia integral do original, incluindo todo o histórico do projeto, completamente funcional por si só. Em especial, você não precisa contatar o primeiro repositório novamente para examinar o histórico do seu clone ou para executar commit nele – entretanto, o seu novo repositório retém uma referência ao original, chamado de “remoto”. Essa referência inclui o estado dos branches no remoto da última vez que você trouxe algo dele; esses são chamados de branches de “monitoramento remoto”. Se o repositório original contiver dois branches chamados master e topic, seus branches monitoramento remoto no seu clone aparecerão qualificados com o nome do remoto (por default chamado de “origin”): origin/master e origin/topic. Na maior parte das vezes, será feita a cópia completa (checkout) do branch master automaticamente para você quando clonar o repositório pela primeira vez; o Git faz o checkout inicialmente de qualquer que seja o branch corrente no repositório remoto. Se você solicitar posteriormente para fazer o checkout do branch topic, o Git verifica que ainda não existe um branch local com esse nome – mas já que existe um branch de monitoramento remoto chamado origin/topic, ele cria automaticamente um branch chamado topic e configura origin/topic como seu branch “upstream”. Esse relacionamento faz com que o mecanismo de push/pull mantenha as mudanças feitas nesses branches em sincronia à medida que se desenvolvem tanto no seu repositório quanto no remoto. Quando você traz algo (pull), o Git atualiza os branches monitoramento remoto com o estado corrente do repositório de origem; de modo inverso, quando você envia algo (push), ele atualiza o remoto com quaisquer mudanças que você tiver feito nos branches locais correspondentes. Se essas mudanças entrarem em conflito, o Git pedirá a você para executar uma fusão das mudanças antes de aceitá-las ou enviá-las, de modo que nenhum lado perca o histórico do processo.
Capítulo 1 ■ Compreendendo o Git
21
Se você estiver familiarizado com o CVS ou o Subversion, uma mudança conceitual útil é considerar que um “commit” nesses sistemas é semelhante a um “push” do Git. Você ainda executa commits no Git, é claro, mas isso afeta apenas o seu repositório e não fica visível para outras pessoas até que você envie esses commits – e você está livre para editar, reorganizar ou excluir seus commits até que o faça.
O depósito de objetos Discutiremos agora em mais detalhes as ideias que acabamos de apresentar, começando com o núcleo de um repositório Git: seu depósito de objetos. Esse é um banco de dados que armazena apenas quatro tipos de itens: blobs, árvores, commits e tags.
Blob Um blob é uma porção opaca de dados, uma string de bytes sem mais estrutura interna no que diz respeito ao Git. O conteúdo de um arquivo em controle de versão é representado como um blob. Isso não significa que a implementação de blobs seja singela. Git usa técnicas sofisticadas de compressão e transmissão para lidar de forma eficiente com blobs. Cada versão de um arquivo no Git é representada como um todo, com seu próprio blob mantendo o conteúdo completo do arquivo. Isso contrasta com alguns outros sistemas, nos quais versões de arquivos são representadas como uma série de diferenças de uma versão para a próxima, iniciando de uma versão base. Esse esquema diferenciado tem seus prós e contras. Se por um lado o Git pode consumir mais espaço em disco, por outro, ele não precisa reconstruir os arquivos para acessá-los, aplicando várias camadas de alterações, resultando assim em um uso mais rápido. Esse projeto aumenta a robustez ao aumentar a redundância: a corrupção de dados em um blob afeta unicamente aquela versão do arquivo, enquanto uma corrupção em uma diferença afeta todas as versões subsequentes.
22
Git Guia Prático
Árvore (Tree) Uma árvore do Git é na verdade o que poderia ser imaginado como um nível de uma árvore: ela representa um único nível de estrutura de diretório no conteúdo do repositório. Contém uma lista de itens; cada um dos quais possui: • Um nome de arquivo para informações associadas que o Git registra, como suas permissões Unix (“mode bits”), e o tipo do arquivo; o Git pode lidar com “links simbólicos” do Unix, assim como com arquivos normais. • Um ponteiro para outro objeto. Se esse objeto for um blob, então esse item representa um arquivo; se for outra árvore, um diretório. Há uma ambiguidade aqui: quando dizemos “árvore”, o que queremos dizer é um único objeto, conforme descrito anteriormente, ou o conjunto de todos os objetos que possam ser alcançados a partir dele seguindo os ponteiros recursivamente até que alcancemos os blobs terminais – ou seja, uma “árvore” no sentido mais habitual? É esta última noção de árvore que essa estrutura de dados é usada para representar, é claro, e, felizmente, raramente é necessário na prática fazer a distinção. Quando dizemos “árvore”, normalmente iremos querer dizer a hierarquia inteira de objetos blob e árvores; quando necessário, usaremos a expressão “objeto árvore” para referirmo-nos especificamente ao componente de estrutura de dados individual. Uma árvore Git, então, representa uma parte do conteúdo do repositório em um determinado momento: um snapshot do conteúdo de um diretório específico, incluindo todos os diretórios abaixo dele. OBSERVAÇÃO: Originalmente, o Git gravava e restaurava as permissões completas em arquivos (todos os mode bits). Posteriormente, porém, isso foi deixado de lado, por causar mais problemas do que benefícios, de modo que a interpretação dos mode bits no índice foi mudada. Agora, os únicos valores válidos para os 12 bits inferiores do modo, conforme armazenados em Git, são octais 755 e 644, e esses apenas indicam que o arquivo deve ser executável ou não. Git configura os bits de execução em um arquivo no checkout de acordo com isso, mas o valor real do modo pode ser diferente, dependendo da sua configuração umask; por exemplo, se o seu umask for 0077, então um arquivo armazenado com modo 755 do Git acabará com modo 700.
Capítulo 1 ■ Compreendendo o Git
23
Commit Um sistema de controle de versão gerencia mudanças no conteúdo, e commit é a unidade fundamental de mudança no Git. Um commit é um snapshot do conteúdo inteiro do repositório junto a informações de identificação, e o relacionamento do estado histórico desse repositório com outros estados gravados à medida que o conteúdo muda ao longo do tempo. Especificamente, um commit consiste de: • Um ponteiro para uma árvore contendo o estado completo do conteúdo do repositório em um determinado momento. • Informações auxiliares sobre esta mudança: quem foi responsável pelo conteúdo (o “autor”); quem introduziu a mudança no repositório (o “committer”); e o dia e a hora em que ocorreu essa mudança. A adição de um objeto commit no repositório é chamada de “executar um commit” ou “commit no repositório”. • Uma lista de zero ou mais de outros objetos commit, chamados de “pais” desse commit. O relacionamento parental não tem significado intrínseco; entretanto as formas normais de executar um commit devem indicar que o estado do repositório do commit foi derivado pelo autor a partir daqueles dos seus pais, de alguma maneira significativa (e.g., acrescentando um recurso ou consertando um problema). Uma cadeia de commits, cada um tendo um único pai, indica uma evolução simples do estado do repositório em passos discretos (conforme veremos, este constitui um branch). Quando um commit possui mais de um pai, isso indica uma “fusão”, na qual o committer incorporou as mudanças a partir de múltiplas linhas de desenvolvimento em um único commit. Definiremos branches e fusões mais precisamente em breve. É claro que pelo menos um commit no repositório deve ter zero pais, senão o repositório seria infinitamente grande ou teria loops no grafo de commits, o que não é permitido (veja a descrição de um “DAG” a seguir). Isso é chamado de “commit raiz” e, com maior frequência, há apenas um commit raiz em um repositório – aquele que foi criado quando o repositório foi iniciado. Entretanto você pode introduzir múltiplos commits raiz se quiser; o comando git checkout --orphan faz isso. Isso incorpora múltiplos históricos independentes a um repositório, talvez para juntar
24
Git Guia Prático
o conteúdo de projetos previamente separados (veja “Importando um histórico desconectado” na página 159).
Autor versus committer Informações separadas de autor e committer – nome, endereço de e-mail e timestamp – refletem a criação do conteúdo do commit e sua adição ao repositório, respectivamente. Elas são inicialmente as mesmas, mas podem se tornar posteriormente distintas com o uso de determinados comandos Git. Por exemplo, git cherry-pick replica um commit existente reaplicando as mudanças introduzidas por esse commit em outro contexto. Cherry-picking passa adiante as informações do autor a partir do commit original, ao mesmo tempo que adiciona novas informações do committer. Isso preserva a identificação e a data da origem das mudanças, ao mesmo tempo que indica que foram aplicadas em outro ponto do repositório em uma data posterior, possivelmente por uma pessoa diferente. Um conserto de erro que sofra cherry-pick de um repositório para outro poderia ter o seguinte formato: $ git log --format=fuller commit d404534d Author: Eustace Maushaven <eustace@qoxp.net> AuthorDate: Thu Nov 29 01:58:13 2012 -0500 Commit: Richard E. Silverman <res@mlitg.com> CommitDate: Tue Feb 26 17:01:33 2013 -0500 Fix spin-loop bug in k5_sendto_kdc In the second part of the first pass over the server list, we passed the wrong list pointer to service_fds, causing it to see only a subset of the server entries corresponding to sel_state. This could cause service_fds to spin if an event is reported on an fd not in the subset. --cherry-picked from upstream by res upstream commit 2b06a22f7fd8ec01fb27a7335125290b8…
Outras operações que fazem isso são git rebase e git filter-branch; assim como git cherry-pick, eles também criam novos commits baseados nos existentes.
Capítulo 1 ■ Compreendendo o Git
25
Assinatura criptográfica Um commit também pode ser assinado usando GnuPG, com: $ git commit --gpg-sign[=keyid]
Veja “Chaves de criptografia” na página 50 sobre o uso de identificadores de chaves pelo Git. Uma assinatura criptográfica associa o commit a uma determinada identidade pessoal no mundo real associada a uma chave usada para assinatura; ela verifica se o conteúdo do commit é o mesmo de quando a pessoa o assinou. O significado da assinatura, porém, é uma questão de interpretação. Se eu assinar um commit, poderia significar que eu analisei o diff, verifiquei se o software funcionava, executei um conjunto de testes e rezei para Cthulhu por um lançamento sem erros; ou posso não ter feito nada disso. Além de ser uma convenção entre os usuários do repositório, também posso colocar o motivo da minha assinatura na mensagem de commit; presumivelmente, não assinarei um commit sem pelo menos ler sua mensagem.
Tag Uma tag serve para distinguir um determinado commit dando a ele um nome que uma pessoa possa ler em um namespace reservado para este propósito. Em caso contrário, os commits são, em um certo sentido, anônimos, normalmente referidos apenas pela sua posição em algum branch, com mudanças ao longo do tempo à medida que o branch se desenvolve (e pode até desaparecer se o branch for excluído posteriormente). O conteúdo da tag consiste no nome da pessoa que a criou, num timestamp, numa referência ao commit que a receberá e num texto em formato livre semelhante a uma mensagem de commit. Uma tag pode ter qualquer significado para você; frequentemente, ela identifica um determinado lançamento de software, com um nome como coolutil-1.0-rc2 e uma mensagem apropriada. Você pode assinar criptograficamente uma tag da mesma forma que o faz com um commit, para verificar a autenticidade dela.
26
Git Guia Prático OBSERVAÇÃO: Há na verdade dois tipos de tags em Git: “lightweight” e “annotated”. Esta seção se refere a tags do tipo annotated, que são representadas como um tipo separado de objeto no banco de dados do repositório. Uma tag lightweight é completamente diferente; é apenas um nome apontando diretamente para um commit (veja a seção a seguir sobre referência para entender como tais nomes funcionam de modo geral).
IDs de objetos e SHA-1 Um elemento de projeto fundamental do Git é que o depósito de objetos usa endereçamento baseado em conteúdo. Alguns outros sistemas atribuem identificadores aos seus equivalentes a commits que são relativos entre si de alguma forma e refletem a ordem na qual os commits são executados. Por exemplo, revisões de arquivos em CVS são strings números e pontos como 2.17.1.3, na qual (geralmente) os números são simplesmente contadores: eles são incrementados à medida que você faz mudanças ou adiciona branches. Isso significa que não existe um relacionamento intrínseco entre uma revisão e seu identificador; a revisão 2.17.1.3 no repositório CVS de outra pessoa, se existir, quase certamente será diferente da sua. O Git, por outro lado, atribui identificadores de objetos baseados no conteúdo de um objeto, em vez de no seu relacionamento com outros objetos, usando uma técnica matemática chamada de função hash. Uma função hash recebe um bloco arbitrário de dados e produz um tipo de impressão digital para ele. A função hash específica que o Git usa, chamada SHA-1, produz um valor de comprimento fixo de 160 bits para qualquer objeto de dados que você enviar para ela, não importa o quão grande seja. A utilidade de identificadores de objetos baseados em hash no Git depende de tratar o hash SHA-1 de um objeto como único; supomos que, se dois objetos possuírem a mesma identidade SHA-1, então são de fato o mesmo objeto. Desta propriedade deriva-se um número de questões-chave:
Depósito de instância única O Git nunca armazena mais de uma cópia de um arquivo. Ele não pode – se você adicionar uma segunda cópia do arquivo, ele executará
Capítulo 1 ■ Compreendendo o Git
27
um hash no conteúdo do arquivo para encontrar seu ID de objeto SHA-1, examinará o banco de dados e descobrirá que ele já está lá. Isso também é consequência da separação do conteúdo do arquivo do seu nome. Árvores mapeiam nomes de arquivos com os blobs em um passo separado, para determinar o conteúdo de um determinado nome de arquivo em um determinado commit, mas o Git não considera o nome ou outras propriedades de um arquivo ao armazená-lo, apenas seu conteúdo.
Comparações eficientes Como parte do gerenciamento de alterações, Git está constantemente executando comparações: arquivos com outros arquivos, arquivos modificados com commits existentes, assim como um commit com outro. Ele compara os estados do depositório inteiro, o qual poderia englobar centenas ou milhares de arquivos, mas faz isso com grande eficiência devido ao hashing. Ao comparar duas árvores, por exemplo, se descobrir que duas subárvores possuem o mesmo ID, pode parar imediatamente de comparar essas partes das árvores, não importa quantos níveis de diretórios e arquivos ainda restem. Por quê? Dizemos anteriormente que um objeto árvore contém “ponteiros” para seus objetos filhos, sejam blobs ou outras árvores. Bem, esses ponteiros são os IDs SHA-1 dos objetos. Se duas árvores possuem o mesmo ID, então elas têm o mesmo conteúdo, o que significa que elas devem conter os mesmos IDs de objetos filhos, o que, por sua vez, significa que esses objetos também devem ser os mesmos! Concluímos que, realmente, os conteúdos inteiros das duas árvores devem ser idênticos se a propriedade de unicidade suposta anteriormente permanecer.
Compartilhamento de banco de dados Repositórios Git podem compartilhar seus bancos de dados de objetos em qualquer nível sem problemas, porque não pode haver aliases; a ligação entre um ID e o conteúdo ao qual ele se refere é imutável. Um repositório não pode mexer no depósito de objetos de outro alterando os dados deste; neste sentido, um depósito de objetos só pode ser expandido, não alterado. Ainda temos de nos preocupar com a remoção de objetos que outro banco de dados esteja usando, mas isso é um problema muito mais fácil de ser resolvido.
28
Git Guia Prático
Muito do poder do Git vem da abordagem baseada em conteúdo – mas, se você pensar um pouco, ela é baseada em uma mentira! Estamos alegando que o hash SHA-1 de um objeto de dados é único, mas isso é matematicamente impossível: em consequência de a saída da função hash ter um comprimento fixo de 160 bits, há exatamente 2160 IDs – mas potencialmente infinitos objetos de dados para executar hash. Tem de haver duplicações, chamadas “colisões hash”. O sistema inteiro parece fatalmente falho. A solução para este problema está no que constitui uma “boa” função hash, e a noção estranha de que, embora SHA-1 não consiga ser matematicamente à prova de colisões, é o que poderíamos chamar efetivamente assim. Para os propósitos práticos do Git, não estou necessariamente preocupado se há de fato outros arquivos que poderiam ter o mesmo ID do meu; o que realmente importa é se algum desses arquivos provavelmente aparecerá no meu projeto ou no de outra pessoa. Talvez todos os outros arquivos tenham comprimento de 10 trilhões de bytes ou nunca corresponderá a algum programa ou texto em alguma linguagem de programação, objeto ou linguagem natural já inventada pela humanidade. Esta é exatamente uma propriedade (entre outras) que os pesquisadores tentam inserir em funções hash: o relacionamento entre mudanças na entrada e na saída é extremamente sensível e completamente imprevisível. Mudar um único bit em um arquivo faz com que seu hash SHA-1 mude radicalmente, e alternar um bit diferente nesse arquivo, ou o mesmo bit em um arquivo diferente, mexerá o hash de uma forma que não terá relacionamento reconhecível com as outras mudanças. Assim, não é que colisões hash de SHA-1 não possam acontecer –, é apenas que acreditamos que elas sejam tão pouco prováveis na prática que simplesmente não nos importamos. É claro que discutir tópicos matemáticos precisos em termos gerais é muito perigoso; esta descrição tem como objetivo explicar o motivo pelo qual nos baseamos em SHA-1 para executar este trabalho, não para provar algo rigorosamente ou mesmo para justificar essas alegações.
Segurança SHA-1 significa “Secure Hash Algorithm 1”, e seu nome reflete o fato de que foi projetado para uso em criptografia. “Hashing” é uma técnica
Capítulo 1 ■ Compreendendo o Git
29
básica em ciência da computação, com aplicações em muitas áreas além de segurança, incluindo processamento de sinais, algoritmos de ordenação e pesquisa, e hardware de rede. Uma função “criptograficamente segura” como SHA-1 tem propriedades distintas, porém relacionadas àquelas já mencionadas com relação ao Git; não é apenas extraordinariamente improvável que duas árvores distintas na prática produzam o mesmo ID de commit, mas também deve ser efetivamente impossível que alguém deliberadamente encontre tais árvores, ou encontre uma segunda árvore com o mesmo ID de uma outra. Estes recursos tornam uma função hash útil em segurança assim como para propósitos mais gerais, já que com elas pode se defender contra adulterações assim como alterações comuns ou acidentais nos dados. Devido a SHA-1 ser uma função hash criptográfica, o Git herda determinadas propriedades de segurança do seu uso de SHA-1 assim como operacionais. Se eu marcar com uma tag um determinado commit de software sensível quanto à segurança, não será viável para um atacante substituir um commit com o mesmo ID no qual ele tenha inserido uma backdoor; desde que eu registre o ID do commit de forma segura e o compare corretamente, o repositório ficará à prova desse tipo de adulteração. Conforme explicado anteriormente, o uso encadeado de SHA-1 faz com que a ID da tag cubra o conteúdo inteiro da árvore do commit marcado com tag. A adição de assinaturas digitais GnuPG permite a pessoas atestar o conteúdo dos estados e o histórico do repositório inteiro de uma forma que seja impraticável forjar. A pesquisa criptográfica está sempre avançando, porém, e o poder computacional aumenta todos os anos; outras funções hash como MD5 que já foram consideradas seguras ficaram obsoletas devido a tais avanços. Na verdade, desenvolvemos versões mais seguras do próprio SHA e, desde quando este texto foi escrito no início de 2013, sérios pontos fracos em SHA-1 foram descobertos há pouco. Os critérios usados para avaliar funções hash para uso em criptografia são bastante conservadores, de modo que essas fraquezas são mais teóricas do que práticas atualmente, mas são, apesar disso, significativas. A boa notícia é que essas falhas criptográficas de SHA-1 não afetarão a utilidade do Git como um sistema de controle de versão em si, ou seja, tornar mais provável na prática que o Git trate commits distintos como idênticos (isso seria desastroso). Elas irão afetar
30
Git Guia Prático
as propriedades de segurança que o Git usufrui como resultado do uso de SHA-1, porém o mais importante é que elas são críticas a um número menor de pessoas (e esses objetivos quanto à segurança podem, na sua maior parte, ser alcançados de outras formas, caso necessário). De qualquer maneira, será possível fazer com que o Git passe a usar outra função hash quando isso se tornar necessário – e, dado o estado atual das pesquisas, provavelmente seria prudente fazê-lo o quanto antes.
Onde ficam os objetos Em um depositório do Git, os objetos são armazenados em .git/objects. Eles podem ser armazenados individualmente como objetos “avulsos” ou por arquivos com nomes de caminhos criados a partir dos seus IDs de objeto: $ find .git/objects -type f .git/objects/08/5cf6be546e0b950e0cf7c530bdc78a6d5a78db .git/objects/0d/55bed3a35cf47eefff69beadce1213b1f64c39 .git/objects/19/38cbe70ea103d7185a3831fd1f12db8c3ae2d3 .git/objects/1a/473cac853e6fc917724dfc6cbdf5a7479c1728 .git/objects/20/5f6b799e7d5c2524468ca006a0131aa57ecce7 ...
Eles também podem ser agrupados em estruturas de dados mais compactas chamadas “pacotes”, que aparecem como pares de arquivos .idx e .pack: $ ls .git/objects/pack/ pack-a18ec63201e3a5ac58704460b0dc7b30e4c05418.idx pack-a18ec63201e3a5ac58704460b0dc7b30e4c05418.pack
O Git reorganiza automaticamente o depósito de objetos ao longo do tempo para melhorar o desempenho; por exemplo, quando ele percebe que há muitos objetos avulsos, junta-os automaticamente em pacotes (embora você possa fazer isso à mão; veja git-repack(1)). Não suponha que os objetos estejam representados de alguma forma específica; sempre use comandos Git para acessar o banco de dados de objetos, em vez de examinar o git por si mesmo.
Capítulo 1 ■ Compreendendo o Git
31
O grafo de commits O conjunto de todos os commits de um repositório forma o que em matemática chamamos de grafo: visualmente, um conjunto de objetos com linhas ligando pares deles. No Git, as linhas representam o relacionamento dos pais do commit explicado anteriormente, e esta estrutura é chamada de “grafo de commits” do repositório. Devido à forma pela qual o Git trabalha, há alguma estrutura adicional neste grafo: as linhas podem ser traçadas com setas apontando em uma direção porque um commit se refere ao seu pai, mas não no sentido inverso (veremos posteriormente a necessidade e significância disso). Usando mais uma vez um termo matemático, isso torna o grafo “direcionado”. O grafo de commits pode ser um histórico linear simples, conforme mostrado na figura 1.2.
Figura 1.2 – Um grafo linear de commits.
Ou uma imagem complexa envolvendo muitos branches e fusões, conforme mostrado na figura 1.3.
Figura 1.3 – Um grafo de commits mais complexo.
Estes são os próximos tópicos que examinaremos.
O que é um DAG? O Git, por projeto, nunca produzirá um grafo que contenha um loop, ou seja, uma forma de seguir as setas de um commit para outro de forma que você passe pelo mesmo commit duas vezes (pense o que isso possivelmente significaria em termos de um histórico de alterações!). Isso é chamado de grafo “acíclico”: não possui um ciclo, ou loop. Assim, o grafo de commits é tecnicamente um “grafo acíclico direcionado”, ou apenas DAG (“directed acyclic graph”).
32
Git Guia Prático
Referências O Git define dois tipos de referências, ou ponteiros com nome, que ele chama de “refs”: • Uma referência simples, que aponta diretamente para o ID de um objeto (geralmente um commit ou tag). • Uma referência simbólica (ou symref), que aponta para outra referência (simples ou simbólica). Essas são semelhantes aos “hard links” e “links simbólicos” em um sistema de arquivos Unix. O Git usa referências para dar nomes a algo, incluindo commits, branches e tags. Elas ficam em um namespace hierárquico separadas por barras (assim como com nomes de arquivos Unix), começando com refs/. Um repositório novo possui pelo menos refs/tags/ e refs/heads/, para guardar os nomes de tags e branches locais, respectivamente. Também existe um refs/remotes/, que armazena nomes que se refiram a outros repositórios; esses contêm abaixo de si os namespaces de referência desses repositórios e são usados em operações de push e pull. Por exemplo, quando você clona um repositório, o Git cria um “remoto” chamado origin referindo-se ao repositório de origem. Há vários padrões, o que significa que você não precisará se referir frequentemente a uma referência pelo seu nome completo; por exemplo, em operações de branches, o Git procura implicitamente o nome que você passar em refs/heads/.
Comandos relacionados Estes são comandos de baixo nível que exibem, alteram e excluem diretamente as referências. Você geralmente não precisará deles, já que o Git geralmente lida automaticamente com referências junto aos objetos que elas representam, como branches e tags. Se você alterar as referências diretamente, assegure-se de saber o que está fazendo! git show-ref
Exibe as referências e os objetos às quais elas se referem.
Capítulo 1 ■ Compreendendo o Git
33
git symbolic-ref
Lida especificamente com referências simbólicas. git update-ref
Altera o valor de uma referência. git for-each-ref
Executa uma ação em um conjunto de referências. AVISO: As referências muitas vezes ficam em arquivos e diretórios correspondentes em .git/refs; entretanto não adquira o hábito de procurálas ou alterá-las diretamente lá, já que há casos nos quais são armazenadas em outros locais (em “pacotes”, na verdade, da mesma forma que com os objetos), e a alteração de uma poderia envolver outras operações sobre as quais você não tem conhecimento. Sempre use comandos Git para manipular referências.
Branches Um branch Git é o que há de mais simples possível: um ponteiro para um commit, como uma referência. Ou então é a sua implementação; o próprio branch é definido como todos os pontos que podem ser alcançados a partir do commit com nome (a “ponta” do branch). A referência especial HEAD determina em qual branch você está; se HEAD for uma referência simbólica para um branch existente, então você “está nesse” branch. Se, por outro lado, HEAD for uma referência simples nomeando um commit diretamente pelo seu ID SHA-1, então você não “está” em algum branch, mas sim no modo “detached HEAD”, o que ocorre quando você executa checkout em algum commit anterior para exame. Vejamos: # HEAD aponta para o branch master
$ git symbolic-ref HEAD refs/heads/master # Git concorda; estou no branch master
$ git branch * master
34
Git Guia Prático # Faz checkout de um commit com tag, # que não está na ponta de um branch
$ git checkout mytag Note: checking out 'mytag'. You are in 'detached HEAD' state... # Confirmado: HEAD não é mais uma referência simbólica
$ git symbolic-ref HEAD fatal: ref HEAD is not a symbolic ref # É o que então? Um ID de commit...
$ git rev-parse HEAD 1c7ed724236402d7426606b03ee38f34c662be27 # ... que corresponde ao commit referenciado pela tag
$ git rev-parse mytag^{commit} 1c7ed724236402d7426606b03ee38f34c662be27 # Git concorda; não estamos em branch algum
$ git branch * (no branch) master
O commit HEAD também é frequentemente chamado de commit “corrente”. Se você estiver em um branch, ele também pode ser chamado de “último” commit ou commit da “ponta” do branch. Um branch se desenvolve ao longo do tempo; assim, se você estiver no branch master e executar um commit, o Git faz o seguinte: 1. Cria um novo commit com suas alterações no conteúdo do repositório. 2. Faz com que o commit na ponta atual do branch master seja o pai do novo commit. 3. Adiciona o novo commit ao depósito de objetos. 4. Altera o branch master (especificamente, a referência refs/heads/master) para que aponte para o novo commit. Em outras palavras, o Git adiciona o novo commit ao final do branch usando o ponteiro pai do commit e avança a referência do branch para o novo commit.
Capítulo 1 ■ Compreendendo o Git
35
Perceba algumas consequências deste modelo: • Considerado individualmente, um commit não é parte intrínseca de algum branch. Não há nada no próprio commit que lhe diga o nome do branch onde ele está ou tenha estado; pertencer a um branch é uma consequência do grafo de commits e os ponteiros do branch corrente. • “Excluir” um branch significa simplesmente excluir a referência correspondente; não tem efeito imediato no depósito de objetos. Em especial, excluir um branch não exclui quaisquer commits. O que isso pode fazer, entretanto, é tornar determinados commits não interessantes, pelo fato de que não estão mais em algum branch (ou seja, não podem mais ser alcançados no grafo de commit a partir de alguma ponta de branch ou tag). Se este estado persistir, o Git acabará removendo tais commits do depósito de objetos como parte da coleta de lixo. Até que isso aconteça, porém, se você tiver um ID de commit abandonado, ainda poderá acessá-lo diretamente pelo seu nome SHA-1; o reflog do Git (git log -g) é útil com relação a isso. • Por esta definição, um branch pode incluir mais de apenas commits executados enquanto estava naquele branch; ele também contém commits de branches que venham para este por meio de uma fusão anterior. Por exemplo: aqui, o branch topic sofreu fusão em master no commit C, e então ambos os branches continuaram a se desenvolver separadamente, conforme mostrado na figura 1.4.
Figura 1.4 – Uma fusão simples.
Neste momento, git log no branch master mostra não apenas os commits de A a D, conforme você esperaria, mas também os commits 1 e 2, já que eles também podem ser alcançados a partir de D por meio de C. Isso pode ser surpreendente, mas é apenas diferente da forma de definição da ideia de um branch: como o conjunto de todos os commits que contribuíram com conteúdo para o último commit. Você normalmente pode obter o efeito de ver “apenas o histórico deste branch” – embora não esteja bem definido – com git log --first-parent.
36
Git Guia Prático
O índice O “índice” do Git muitas vezes parece um pouco misterioso para as pessoas: algum lugar invisível e difícil de definir, onde as alterações ficam guardadas (são “staged”) até sofrerem commit. A conversa sobre “guardar alterações” no índice também sugere que ele armazena apenas alterações, como se fosse um conjunto de diffs esperando para serem aplicados. A verdade é diferente e bastante simples, e é importante que seja entendida para que se compreenda bem o Git. O índice é uma estrutura de dados independente, separada tanto da sua árvore de trabalho quanto de qualquer commit. É simplesmente uma lista de nomes de caminhos de arquivos junto a atributos associados, geralmente incluindo o ID de um blob no banco de dados de objetos que armazena dados para uma versão desse arquivo. Você pode ver o conteúdo corrente do índice com git ls-files: $ git ls-files --abbrev --stage 100644 2830ea0b 0 TODO 100644 a4d2acee 0 VERSION 100644 ce30ff91 0 acinclude.m4 100644 236d5f93 0 configure.ac ...
A opção --stage significa mostrar apenas o índice; git ls-files pode mostrar diversas combinações e subconjuntos do índice e sua árvore de trabalho. Se você fosse excluir ou alterar algum dos arquivos listados na sua árvore de trabalho, isso não afetaria o resultado desse comando; ele não as examina. Fatores importantes sobre o índice: • O índice é a fonte implícita do conteúdo de um commit normal. Se você usa git commit (sem fornecer nomes de caminhos específicos), talvez possa pensar que ele cria esse novo commit baseado nos seus arquivos de trabalho. Ele não faz isso; em vez disso, ele simplesmente vê o índice corrente como um novo objeto árvore e cria o novo commit a partir dele. É por isso que você precisa executar “stage” em um arquivo alterado no índice com git add para que ele faça parte do seu próximo commit. • O índice não contém apenas alterações a serem feitas no próximo commit; ele é o próximo commit, um catálogo completo dos arquivos que serão incluídos na árvore do próximo commit (lembre-se de
Capítulo 1 ■ Compreendendo o Git
37
que cada commit se refere a um objeto árvore que é um snapshot completo do conteúdo do repositório). Quando você executa checkout em um branch, o Git reinicializa o índice para corresponder ao commit da ponta desse branch; você então modifica o índice com comandos como git add/mv/rm para indicar as alterações a fazer parte do próximo commit. • git add não observa apenas no índice que um arquivo foi alterado; ele na verdade adiciona o conteúdo do arquivo corrente ao banco de dados de objetos como um novo blob e atualiza a entrada do índice para se referir a esse blob. É por isso que git commit é sempre rápido, mesmo se você estiver fazendo muitas alterações: todos os dados reais foram armazenados por comandos git add anteriores. Uma implicação deste comportamento que de vez em quando confunde as pessoas é que, se você alterar um arquivo, executar git add e depois alterá-lo novamente, é a versão que você adicionou por último ao índice, não àquela na sua árvore de trabalho, que faz parte do próximo commit. git status mostra isso explicitamente, listando o mesmo arquivo tanto em “changes to be committed” como em “changes not staged for commit”. • Semelhante ao git commit, o git diff sem argumentos também tem o índice como um operando implícito; ele mostra as diferenças entre sua árvore de trabalho e o índice, em vez do commit corrente. Inicialmente esses são o mesmo, já que o índice corresponde ao último commit após um commit ou checkout limpo. À medida que você altera seus arquivos de trabalho, esses aparecem na saída de git diff, depois desaparecem quando você adiciona os arquivos correspondentes. A ideia é que git diff mostre as alterações que ainda não sofreram stage para commit, de modo que você possa ver com o que ainda tem de lidar (ou o que deliberadamente não incluiu) quando preparar seu próximo commit. git diff --staged mostra o contrário: as diferenças entre o índice e o commit corrente (ou seja, as alterações que sofrerão commit).
38
Git Guia Prático
Executando fusões A fusão (merging) é o complemento do branching no controle de versão: um branch permite a você trabalhar simultaneamente com outros em um determinado conjunto de arquivos, enquanto uma fusão permite que você combine posteriormente trabalhos separados em dois ou mais branches que divergiam anteriormente a partir de um ancestral comum. Aqui estão dois cenários comuns de fusão: 1. Você está trabalhando sozinho em um projeto de software. Decide explorar o refatoramento no seu código de uma certa forma, e então cria um branch chamado refactor a partir do master. Você pode fazer quaisquer alterações que quiser no branch refactor sem atrapalhar a linha principal de desenvolvimento.
Após um certo tempo, você fica satisfeito com o refatoramento que executou e quer mantê-lo, então vai até o branch master e executa git merge refactor. O Git aplica as alterações que você fez em ambos os branches desde que eles divergiram, solicitando sua ajuda para resolver alguns conflitos, e então executa commit no resultado. Você exclui o branch refactor e segue em frente.
2. Você está trabalhando no branch master de um repositório clonado e executou diversos commits em um dia ou dois. Você executa então git pull para atualizar seu clone com a versão mais recente do trabalho que sofreu commit no repositório original. Acontece que outras pessoas também executaram commit no branch master original nesse meio-tempo, de forma que o Git executa uma fusão automática de master e origin/master e executa commit no seu branch master. Você pode continuar então com seu trabalho ou executar push para o repositório origin agora que incorporou suas alterações mais recentes às suas. Veja “Push e pull” na página 40. Há dois aspectos na fusão com o Git: conteúdo e histórico.
Fusão de conteúdo O que significa uma “fusão” bem-sucedida de dois ou mais conjuntos de alterações no mesmo arquivo depende da natureza do conteúdo. O Git tenta executar a fusão automaticamente e muitas vezes a considera
Capítulo 1 ■ Compreendendo o Git
39
bem-sucedida se os dois conjuntos de alterações alteraram partes independentes do arquivo. Se você vai considerar isso um sucesso, porém, é uma questão diferente. Se o arquivo for o capítulo três do seu próximo romance, então talvez tal fusão seja apropriada se você estiver fazendo pequenas correções na gramática ou de estilo. Se você estivesse reescrevendo alguma parte essencial, por outro lado, o resultado poderia ser menos útil – talvez você tenha adicionado um parágrafo em um branch que dependia de detalhes contidos em um parágrafo posterior que foi excluído em outro branch. Mesmo se os conteúdos forem código de programação, tal fusão não tem garantia de ser útil. Você poderia alterar duas sub-rotinas separadas de uma forma que fizesse com que elas falhassem ao serem usadas; elas poderiam estar fazendo agora suposições incompatíveis sobre alguma estrutura de dados compartilhada, por exemplo. O Git nem verifica se o seu código ainda compila, isso é com você. Dentro dessas limitações, porém, o Git possui mecanismos muito sofisticados para mostrar os conflitos de fusão e ajudá-lo a resolvê-los. Ele é otimizado para o caso de uso mais comum: dados de textos orientados a linha, frequentemente em linguagens de programação. Ele possui diferentes estratégias e opções para determinar partes “correspondentes” de arquivos, que você pode usar quando o comportamento padrão não produzir resultados adequados. Você pode escolher interativamente conjuntos de alterações para aplicar, pular ou editar mais. Para lidar com fusões complexas, o Git trabalha tranquilamente com ferramentas de fusão externas como araxis, emerge e kdiff, ou com ferramentas personalizadas de fusão que você mesmo escrever.
Executando a fusão dos históricos Quando o Git tiver terminado tudo o que puder fazer automaticamente, e você tiver resolvido qualquer conflito que tenha ficado para trás, é sinal de que está na hora de executar commit no resultado. Se apenas executarmos um commit no branch corrente, como de hábito, porém, perderemos informações críticas: o fato de ter acontecido uma fusão, e quais branches estavam envolvidos. Você talvez lembre de incluir estas informações na mensagem de commit, mas é melhor não depender disso; mais importante ainda, o Git precisa saber a respeito da fusão para
40
Git Guia Prático
executar boas fusões no futuro. Caso contrário, da próxima vez em que você executar uma fusão nos mesmos branches (digamos, para atualizar periodicamente um com as alterações do outro), o Git não saberá quais alterações já sofreram fusão e quais são novas. Ele pode acabar marcando como conflitos alterações que você já havia analisado e com as quais tenha lidado, ou aplicar automaticamente alterações que você já havia decidido descartar. A forma pela qual o Git registra uma fusão é muito simples. Lembre-se do “Depósito de objetos” na página 21, em que vimos que um commit possui uma lista de zero ou mais “commits pais”. O commit inicial de um repositório não possui pais, e um commit simples em um branch possui apenas um. Quando você faz um commit que seja parte de uma fusão, o Git lista os commits da ponta de todos os branches envolvidos como sendo pais do novo commit. Esta é na verdade a definição de um “commit de fusão”: um commit com mais de um pai. Essa informação, registrada como parte do grafo de commits, permite que ferramentas de visualização detectem e exibam fusões de uma forma útil, e não ambígua. Permite também ao Git encontrar uma versão básica apropriada para comparação com fusões posteriores no mesmo branch ou em branches relacionados quando eles divergirem novamente, evitando a duplicação mencionada anteriormente; isso é chamado de “base de fusão”.
Push e pull Você usa os comandos git pull e git push para atualizar o estado de um repositório a partir de outro. Normalmente, um desses repositórios foi clonado do outro; neste contexto, git pull atualiza o meu clone com o trabalho recente adicionado ao repositório original, enquanto git push contribui com o meu trabalho na outra direção. Às vezes, há confusão em relação ao relacionamento entre um repositório e aquele a partir do qual ele foi clonado. Disseram-nos que todos os repositórios são iguais, mas parece haver uma assimetria no relacionamento original/clone. Executar pull automaticamente atualiza esse repositório a partir do original, então o quão interconectados eles estão? O clone ainda estará usável se o original não existir mais? Há branches no meu
Capítulo 1 ■ Compreendendo o Git
41
repositório que sejam de algum modo ponteiros para o conteúdo em outro repositório? Se houver, isso não parece indicar que eles sejam verdadeiramente independentes. Felizmente, assim como de costume no Git, a situação é na verdade muito simples; só precisamos definir exatamente os termos à mão. O ponto central a ser lembrado é que, com relação ao conteúdo, um repositório consiste de dois componentes: um depósito de objetos e um conjunto de referências – ou seja, um grafo de commits e um conjunto de nomes de branches e tags que chamam os commits que interessem. Quando você clona um repositório, como com git clone server:dir/repo, o Git faz o seguinte: 1. Cria um novo repositório. 2. Adiciona um remote chamado “origin” para se referir ao repositório que estiver sendo clonado em .git/config: [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = server:dir/repo
O valor de fetch aqui, chamado de refspec, especifica uma correspondência entre conjuntos de referências nos dois repositórios: o padrão no lado esquerdo dos dois-pontos dá o nome das referências no remoto, e a especificação indica com o padrão no lado direito onde as referências correspondentes devem aparecer no repositório local. Neste caso, significa: “Mantenha cópias das referências do branch do origin remoto no seu namespace local neste repositório, refs/remotes/origin/.” 3. Executa git fetch origin, que atualiza nossas referências locais para os branches do remoto (criando-os, neste caso) e solicita que o remoto envie quaisquer objetos que precisemos para completar o histórico dessas referências (no caso deste repositório novo, todos eles). 4. Finalmente, o Git executa checkout do branch corrente do remote (sua referência HEAD), deixando-o com uma árvore de trabalho para examinar. Você pode selecionar um branch inicial diferente para executar checkout com --branch ou suprimir totalmente esse checkout com -n.
42
Git Guia Prático
Suponha que você saiba que o outro repositório tem dois branches, master e beta. Tendo clonado o repositório, vemos: $ git branch * master
Muito bem, estamos no branch master, mas onde está o branch beta? Ele permanece como se estivesse ausente até que usemos a opção --all: $ git branch --all * master remotes/origin/HEAD -> origin/master remotes/origin/master remotes/origin/beta
Ahá! Lá está ele. Isto faz algum sentido: temos cópias das referências de ambos os branches no repositório de origem, onde o refspec do origin diz que ele deveria estar, e também há a referência HEAD a partir do origin, o que informou ao Git o branch padrão a sofrer checkout. O que é curioso: o que é esta duplicata do branch master, fora do origin, que é onde realmente estamos? E por que temos de passar uma opção extra para ver todos eles? A resposta está no propósito das referências origin: elas são chamadas de referências de monitoramento remoto, e são os marcadores mostrando o estado corrente desses branches no remoto (desde a última vez que os verificamos com o remoto por meio de um fetch ou pull). Ao adicionar ao branch master, você não quer atualizar diretamente seu branch de monitoramento com um commit próprio; ele não iria mais refletir o estado do repositório remoto (e, na sua próxima execução de pull, ele descartaria suas adições reinicializando o branch de monitoramento para que corresponda ao remoto). Assim, o Git criou um novo branch com o mesmo nome em seu namespace local, iniciando no mesmo commit do branch remoto: $ git show-ref --abbrev master d2e46a81 refs/heads/master d2e46a81 refs/remotes/origin/master
Capítulo 1 ■ Compreendendo o Git
43
Os valores SHA-1 abreviados à esquerda são os IDs dos commits; observe que eles são os mesmos, e lembre-se de que refs/heads/ é o namespace implícito para branches locais. Quando você adiciona ao seu branch master, ele divergirá do master remoto, que reflete a situação atual. O componente final aqui é o comportamento do seu branch master local com relação ao remoto. Sua intenção é, presume-se, compartilhar seu trabalho com outros como uma atualização nos seus branches master; além disso, você gostaria de acompanhar as alterações feitas neste branch no remoto enquanto estiver trabalhando. Para isso, o Git adicionou alguma configuração para este branch em .git/config: [branch "master"] remote = origin merge = refs/heads/master
Isso significa que, quando você usa git pull estando neste branch, o Git tenta executar automaticamente fusões entre quaisquer alterações feitas no branch remoto correspondente desde a última execução de pull. Essa configuração afeta o comportamento de outros comandos também, incluindo fetch, push e rebase. Finalmente, o Git possui um recurso conveniente especial para git checkout se você quiser tentar executar checkout em um branch que não exista, mas exista um branch correspondente como parte de um remoto. Ele configurará automaticamente um branch local com o mesmo nome com a configuração upstream recém-demonstrada. Por exemplo: $ git checkout beta Branch beta set up to track remote branch beta from origin. Switched to a new branch 'beta' $ git branch --all * beta master remotes/origin/HEAD -> origin/master remotes/origin/beta remotes/origin/master
Tendo explicado o monitoramento remoto de branches, agora podemos dizer sucintamente o que fazem as operações push e pull:
44
Git Guia Prático
git pull
Executa git fetch no remoto para o branch corrente, atualizando as referências de monitoramento local do remoto e obtendo quaisquer objetos novos que sejam necessários para completar o histórico dessas referências, ou seja, todos os commits, tags e blobs que puderem ser alcançados a partir de novas pontas do branch. Ele tenta então atualizar o branch local corrente para corresponder ao branch no remoto. Se apenas um lado tiver adicionado conteúdo ao branch, então isso será bem-sucedido e será chamado de atualização fast-forward, já que uma referência é simplesmente movida pelo branch para corresponder à outra. Se ambos os lados tiverem executado commit no branch, porém, então o Git terá de fazer algo para incorporar ambas as versões do histórico do branch em uma versão compartilhada. Por padrão, esta é uma fusão: o Git executa a fusão do branch remoto com o local, produzindo um novo commit que se refere a ambos os lados do histórico por meio dos seus ponteiros pais. Outra possibilidade é executar rebase, que tenta reescrever seus commits divergentes com novos na ponta do branch remoto atualizado (veja “Pull com rebase” na página 98). git push
Tenta atualizar o branch correspondente no remoto com o seu estado local, enviando quaisquer objetos que o remoto precise para completar o novo histórico. Isso falhará se a atualização não for fast-forward, conforme descrito anteriormente (i.e., faria com que o remote descartasse o histórico), e o Git sugerirá que você primeiro execute pull para resolver as discrepâncias e produzir uma atualização aceitável.
Observações 1. Deve ficar claro a partir desta descrição que nada relacionado a branches de monitoramento remoto conecta a operação do seu repositório ao remoto. Cada um é apenas um branch no seu repositório, como qualquer outro branch, uma referência apontando para um determinado commit. Eles só são “remotos” na sua intenção: monitoram o estado dos branches correspondentes no remoto e são atualizados periodicamente por meio de git pull.
Capítulo 1 ■ Compreendendo o Git
45
2. Isso pode ficar momentaneamente confuso se você clonar um repositório, usar git log em um branch que sabe que está no remoto e ele falhar – porque você não tem um branch local com esse nome (ainda); ele está apenas no remoto. Você não tem que executar checkout e configurar um branch local apenas para examiná-lo; pode especificar o branch de monitoramento remoto pelo nome: git log origin/foo. 3. Um repositório pode ter qualquer quantidade de remotos, configurados a qualquer momento; veja git-remote(1). Se o repositório original que você tiver clonado não for mais válido, você pode corrigir a URL editando .git/config ou com git remote set-url, ou removê-lo inteiramente com git remote rm (o que removerá os branches correspondentes de monitoramento remoto também).