Chapters ▾ 2nd Edition

7.1 Ferramentas do Git - Seleção de Revisão

A esta altura, você aprendeu a maioria dos comandos e fluxos de trabalho do dia a dia de que precisa para gerenciar ou manter um repositório Git para o seu controle de código-fonte. Você concluiu as tarefas básicas de rastreamento e commit de arquivos, e utilizou o poder da área de stage, do branch de tópicos leve (lightweight topic branching) e merge.

Agora você explorará várias coisas muito poderosas que o Git pode fazer, as quais você pode não usar necessariamente no dia a dia, mas pode precisar em algum momento.

Seleção de Revisão

O Git permite que você se refira a um único commit, conjunto de commits ou intervalo (range) de commits de várias maneiras. Elas não são necessariamente óbvias, mas são úteis de se conhecer.

Revisões Únicas

Você pode obviamente se referir a qualquer commit único pelo seu hash SHA-1 completo de 40 caracteres, mas também existem formas mais amigáveis aos humanos de se referir a commits. Esta seção descreve as várias maneiras pelas quais você pode se referir a qualquer commit.

SHA-1 Curto

O Git é inteligente o suficiente para descobrir a qual commit você está se referindo se você fornecer os primeiros caracteres do hash SHA-1, desde que esse hash parcial tenha pelo menos quatro caracteres e seja inequívoco; ou seja, nenhum outro objeto no banco de dados de objetos pode ter um hash que comece com o mesmo prefixo.

Por exemplo, para examinar um commit específico onde você sabe que adicionou certa funcionalidade, você pode primeiro executar o comando git log para localizar o commit:

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    Fix refs handling, add gc auto, update tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    Add some blame and merge stuff

Neste caso, digamos que você esteja interessado no commit cujo hash começa com 1c002dd…​. Você pode inspecionar esse commit com qualquer uma das seguintes variações de git show (supondo que as versões mais curtas sejam inequívocas):

$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d

O Git pode descobrir uma abreviação curta e única para os seus valores SHA-1. Se você passar --abbrev-commit para o comando git log, a saída usará valores mais curtos, mas os manterá únicos; o padrão é usar sete caracteres, mas os torna mais longos se necessário para manter o SHA-1 inequívoco:

$ git log --abbrev-commit --pretty=oneline
ca82a6d Change the version number
085bb3b Remove unnecessary test code
a11bef0 Initial commit

Geralmente, oito a dez caracteres são mais do que suficientes para serem únicos em um projeto. Por exemplo, a partir de fevereiro de 2019, o kernel do Linux (que é um projeto bastante considerável) tem mais de 875.000 commits e quase sete milhões de objetos no seu banco de dados de objetos, sem que dois objetos tenham SHA-1 idênticos nos primeiros 12 caracteres.

Note
UMA BREVE NOTA SOBRE O SHA-1

Muitas pessoas começam a se preocupar em algum momento de que elas, por acaso, terão dois objetos distintos em seu repositório com o mesmo valor de hash SHA-1. E aí?

Se você por acaso comitar um objeto que tenha o mesmo valor de hash SHA-1 que um objeto anterior diferente no seu repositório, o Git verá o objeto anterior já no seu banco de dados do Git, assumirá que ele já foi gravado e simplesmente o reutilizará. Se você tentar fazer o checkout desse objeto novamente em algum momento, você sempre obterá os dados do primeiro objeto.

No entanto, você deve estar ciente de quão ridiculamente improvável é esse cenário. O digest SHA-1 tem 20 bytes ou 160 bits. O número de objetos com hash aleatório necessários para garantir uma probabilidade de 50% de uma única colisão é cerca de 280 (a fórmula para determinar a probabilidade de colisão é p = (n(n-1)/2) * (1/2^160)). 280 é 1,2 x 1024 ou 1 milhão de bilhões de bilhões. Isso é 1.200 vezes o número de grãos de areia na Terra.

Aqui está um exemplo para se ter uma ideia do que seria necessário para obter uma colisão SHA-1. Se todos os 6,5 bilhões de humanos na Terra estivessem programando, e a cada segundo, cada um estivesse produzindo código que fosse o equivalente a toda a história do kernel do Linux (6,5 milhões de objetos Git) e enviando-o para um enorme repositório Git, demoraria cerca de 2 anos até que o repositório contivesse objetos suficientes para ter 50% de probabilidade de uma única colisão de objeto SHA-1. Assim, uma colisão orgânica do SHA-1 é menos provável do que todos os membros da sua equipe de programação serem atacados e mortos por lobos em incidentes não relacionados na mesma noite.

Se você dedicar milhares de dólares em poder computacional a isso, é possível sintetizar dois arquivos com o mesmo hash, como comprovado em https://shattered.io/ em fevereiro de 2017. O Git está caminhando para o uso do SHA256 como algoritmo de hashing padrão, o qual é muito mais resiliente a ataques de colisão, e tem código no lugar para ajudar a mitigar este ataque (embora não possa eliminá-lo completamente).

Referências de Branch

Uma maneira direta de se referir a um commit em particular é se ele for o commit na ponta (tip) de um branch; nesse caso, você pode simplesmente usar o nome do branch em qualquer comando do Git que espere uma referência a um commit. Por exemplo, se você quiser examinar o último objeto de commit em um branch, os seguintes comandos são equivalentes, assumindo que o branch topic1 aponte para o commit ca82a6d…​:

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

Se você quiser ver para qual SHA-1 específico um branch aponta, ou se você quiser ver o que qualquer um desses exemplos se resume em termos de SHA-1s, você pode usar uma ferramenta de encanamento (plumbing) do Git chamada rev-parse. Você pode ver Git Internals (Por Dentro do Git) para mais informações sobre ferramentas de plumbing; basicamente, rev-parse existe para operações de nível mais baixo e não foi projetado para ser usado em operações do dia a dia. No entanto, às vezes pode ser útil quando você precisar ver o que realmente está acontecendo. Aqui você pode rodar o rev-parse no seu branch.

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

Nomes Curtos do RefLog

Uma das coisas que o Git faz em segundo plano enquanto você está trabalhando é manter um “reflog” — um log de onde o seu HEAD e referências de branch estiveram nos últimos meses.

Você pode ver o seu reflog usando git reflog:

$ git reflog
734713b HEAD@{0}: commit: Fix refs handling, add gc auto, update tests
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by the 'recursive' strategy.
1c002dd HEAD@{2}: commit: Add some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD

Toda vez que a ponta do seu branch é atualizada por qualquer motivo, o Git armazena essa informação para você neste histórico temporário. Você também pode usar seus dados do reflog para se referir a commits mais antigos. Por exemplo, se você quiser ver o quinto valor anterior do HEAD do seu repositório, você pode usar a referência @{5} que você vê na saída do reflog:

$ git show HEAD@{5}

Você também pode usar essa sintaxe para ver onde um branch estava há um tempo específico. Por exemplo, para ver onde o seu branch master estava ontem, você pode digitar:

$ git show master@{yesterday}

Isso mostraria a você onde a ponta do seu branch master estava ontem. Esta técnica só funciona para dados que ainda estão no seu reflog, então você não pode usá-la para procurar commits mais antigos que alguns meses.

Para ver as informações do reflog formatadas como a saída do git log, você pode executar git log -g:

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: Fix refs handling, add gc auto, update tests
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    Fix refs handling, add gc auto, update tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

É importante notar que as informações do reflog são estritamente locais — é um log apenas do que você fez no seu repositório. As referências não serão as mesmas na cópia do repositório de outra pessoa; além disso, logo após você clonar um repositório inicialmente, você terá um reflog vazio, já que nenhuma atividade ocorreu ainda no seu repositório. Rodar git show HEAD@{2.months.ago} mostrará o commit correspondente apenas se você clonou o projeto há pelo menos dois meses — se você clonou mais recentemente do que isso, verá apenas seu primeiro commit local.

Tip
Pense no reflog como a versão do Git do histórico do shell

Se você tem experiência em UNIX ou Linux, pode pensar no reflog como a versão do Git do histórico do shell, o que enfatiza que o que está lá é claramente relevante apenas para você e sua “sessão”, e não tem nada a ver com qualquer outra pessoa que possa estar trabalhando na mesma máquina.

Note
Escapando chaves no PowerShell

Ao usar o PowerShell, as chaves como { e } são caracteres especiais e devem ser escapados. Você pode escapá-los com uma crase ` ou colocar a referência do commit entre aspas:

$ git show HEAD@{0}     # will NOT work
$ git show HEAD@`{0`}   # OK
$ git show "HEAD@{0}"   # OK

Referências de Ascendência (Ancestry)

A outra maneira principal de especificar um commit é por meio de sua ascendência. Se você colocar um ^ (acento circunflexo) no final de uma referência, o Git resolverá isso para significar o pai daquele commit. Suponha que você observe o histórico do seu projeto:

$ git log --pretty=format:'%h %s' --graph
* 734713b Fix refs handling, add gc auto, update tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd Add some blame and merge stuff
|/
* 1c36188 Ignore *.gem
* 9b29157 Add open3_detach to gemspec file list

Então, você pode ver o commit anterior especificando HEAD^, que significa “o pai do HEAD”:

$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'
Note
Escapando o acento circunflexo no Windows

No Windows em cmd.exe, ^ é um caractere especial e precisa ser tratado de forma diferente. Você pode duplicá-lo ou colocar a referência do commit entre aspas:

$ git show HEAD^     # will NOT work on Windows
$ git show HEAD^^    # OK
$ git show "HEAD^"   # OK

Você também pode especificar um número após o ^ para identificar qual pai você deseja; por exemplo, d921970^2 significa “o segundo pai de d921970”. Esta sintaxe é útil apenas para commits de merge, que têm mais de um pai — o primeiro pai de um commit de merge é do branch em que você estava quando fez o merge (frequentemente master), enquanto o segundo pai de um commit de merge é do branch que foi mesclado (digamos, topic):

$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    Add some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date:   Wed Dec 10 22:22:03 2008 +0000

    Some rdoc changes

A outra principal especificação de ascendência é o ~ (til). Isto também se refere ao primeiro pai, então HEAD~ e HEAD^ são equivalentes. A diferença torna-se aparente quando você especifica um número. HEAD~2 significa “o primeiro pai do primeiro pai” ou “o avô” — ele percorre os primeiros pais o número de vezes que você especificar. Por exemplo, no histórico listado anteriormente, HEAD~3 seria:

$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    Ignore *.gem

Isso também pode ser escrito como HEAD~~~, que novamente é o primeiro pai do primeiro pai do primeiro pai:

$ git show HEAD~~~
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    Ignore *.gem

Você também pode combinar essas sintaxes — você pode obter o segundo pai da referência anterior (supondo que tenha sido um commit de merge) usando HEAD~3^2, e assim por diante.

Intervalos (Ranges) de Commits

Agora que você pode especificar commits individuais, vamos ver como especificar intervalos de commits. Isso é particularmente útil para gerenciar seus branches — se você tem muitos branches, pode usar as especificações de intervalo para responder a perguntas como: “Qual trabalho está neste branch que eu ainda não mesclei (merged) no meu branch principal?”

Ponto Duplo (Double Dot)

A especificação de intervalo mais comum é a sintaxe de ponto duplo. Isto basicamente pede ao Git para resolver um intervalo de commits que são alcançáveis a partir de um commit, mas não são alcançáveis a partir de outro. Por exemplo, digamos que você tenha um histórico de commits parecido com Exemplo de histórico para seleção de intervalo.

Exemplo de histórico para seleção de intervalo
Figure 136. Exemplo de histórico para seleção de intervalo

Digamos que você queira ver o que está no seu branch experiment que ainda não foi mesclado (merged) no seu branch master. Você pode pedir ao Git para mostrar um log de apenas esses commits com master..experiment — isso significa “todos os commits alcançáveis a partir de experiment que não são alcançáveis a partir de master.” Por uma questão de brevidade e clareza nesses exemplos, as letras dos objetos de commit do diagrama são usadas no lugar da saída de log real, na ordem em que seriam exibidas:

$ git log master..experiment
D
C

Se, por outro lado, você quiser ver o oposto — todos os commits no master que não estão no experiment — você pode inverter os nomes dos branches. experiment..master mostra tudo o que está no master e não é alcançável a partir do experiment:

$ git log experiment..master
F
E

Isso é útil se você quiser manter o branch experiment atualizado e visualizar o que está prestes a mesclar. Outro uso frequente dessa sintaxe é ver o que você está prestes a fazer push (enviar) para um repositório remoto:

$ git log origin/master..HEAD

Este comando mostra quaisquer commits no seu branch atual que não estão no branch master no seu repositório remoto origin. Se você executar um git push e o seu branch atual estiver rastreando origin/master, os commits listados por git log origin/master..HEAD são os commits que serão transferidos para o servidor. Você também pode omitir um lado da sintaxe para fazer com que o Git assuma HEAD. Por exemplo, você pode obter os mesmos resultados do exemplo anterior digitando git log origin/master.. — o Git substitui HEAD se um dos lados estiver faltando.

Múltiplos Pontos

A sintaxe de ponto duplo é útil como um atalho, mas talvez você queira especificar mais do que dois branches para indicar a sua revisão, como ver quais commits estão em qualquer um de vários branches que não estão no branch em que você se encontra atualmente. O Git permite que você faça isso usando o caractere ^ ou --not antes de qualquer referência da qual você não deseja ver os commits alcançáveis. Assim, os três comandos a seguir são equivalentes:

$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

Isso é legal porque com essa sintaxe você pode especificar mais de duas referências na sua consulta, o que não pode fazer com a sintaxe de ponto duplo. Por exemplo, se você quiser ver todos os commits que são alcançáveis a partir do refA ou refB mas não do refC, você pode usar qualquer um destes:

$ git log refA refB ^refC
$ git log refA refB --not refC

Isso cria um sistema de consulta de revisão muito poderoso que deve ajudá-lo a descobrir o que há nos seus branches.

Ponto Triplo (Triple Dot)

A última grande sintaxe de seleção de intervalo é a sintaxe de ponto triplo, que especifica todos os commits que são alcançáveis por qualquer uma das duas referências, mas não por ambas. Veja novamente o histórico de commits de exemplo em Exemplo de histórico para seleção de intervalo. Se você quiser ver o que está no master ou experiment, mas sem quaisquer referências comuns, você pode executar:

$ git log master...experiment
F
E
D
C

Mais uma vez, isso fornece a saída normal de log, mas mostra apenas as informações de commit para aqueles quatro commits, aparecendo na ordem tradicional de data de commit.

Um switch comum para usar com o comando log neste caso é o --left-right, que mostra em qual lado do intervalo cada commit está. Isso ajuda a tornar a saída mais útil:

$ git log --left-right master...experiment
< F
< E
> D
> C

Com essas ferramentas, você pode informar ao Git de forma muito mais fácil qual ou quais commits você deseja inspecionar.