- Um commit em um repositório git registra uma fotografia (snapshot) de todos os arquivos no seu diretório. É como um grande copy&paste, mas ainda melhor!
- O Git tem por objetivo manter os commits tão leves quanto possível, de forma que ele não copia cegamente o diretório completo toda vez que você commita. Ele pode (quando possível) comprimir um commit como um conjunto de mudanças (ou um "delta") entre uma versão do seu repositório e a seguinte.
- O Git também mantém um histórico de quando ocorreu cada commit. É por isso que a maioria dos commits tem ancestrais acima de si -- que indicamos usando setas na nossa visualização. Manter a história é ótimo para todos que trabalham no projeto!
- Há muito para aprender, mas por enquanto pense nos commits como snapshots do seu projeto. Os commits são muito leves, e mudar de um para outro é extremamente rápido!
- Vejamos o que isso significa na prática. Abaixo, temos uma visualização de um (pequeno) repositório git. Há dois commits no momento: o commit inicial,
C0
, e um commit que se segue,C1
, que poderia conter algumas mudanças interessantes.
$ git commit
- Acabamos de fazer mudanças no repositório e as guardamos como um commit. O commit que acabamos de criar tem um pai,
C1
, que referencia em qual commit ele se baseou.
- Ramos no Git também são incrivelmente leves. Eles são simplesmente referências a um commit específico -- e nada mais. É por isso que muitos entusiastas do Git entoam o mantra:
ramifique cedo, ramifique sempre
-
Devido a não existir sobrecarga de armazenamento / memória associada à criação de ramos, é mais fácil dividir logicamente o seu trabalho do que ter ramos grandes e gordos.
-
Quando começarmos a misturar ramos e commits, vamos ver como esses dois recursos combinam bem. Por enquanto, só lembre que um ramo diz essencialmente "Quero incluir o trabalho deste commit e de todos os seus ancestrais".
-
Vejamos como os ramos funcionam na prática. Aqui vamos criar um novo ramo chamado
newImage
$ git branch newImage
- Veja, é só isso que você tem que fazer para ramificar! O ramo
newImage
agora se refere ao commit ` - Vamos tentar colocar algum trabalho neste novo ramo.
$ git commit
- Ah não! O ramo
master
se moveu mas onewImage
não! Isso é porque o novo ramo não era o "ativo", e é por isso que o asterisco (*) estava no master. - Vamos contar ao git que queremos fazer
checkout
no ramo com:
$ git checkout [nome]
$ git checkout newImage; git commit
- Aqui vamos nós! Nossas mudanças foram gravadas no novo ramo
-
Ótimo! Agora sabemos como commitar e criar ramos. Agora precisamos aprender uma forma de combinar o trabalho de dois ramos diferentes. Isso nos permitirá ramificar, desenvolver um novo recurso, e então combiná-lo de volta.
-
O primeiro método para combinar trabalho que vamos examinar é o
git merge
. O merge do Git cria um commit especial que possui dois pais únicos. Um commit com dois pais essencialmente significa "Quero incluir todo o trabalho deste pai aqui com o daquele outro pai ali, e com o do conjunto de todos os seus ancestrais."
- Aqui nós temos dois ramos; cada um tem um
commit
que é único. Isso significa que nenhum ramo inclui o conjunto do "trabalho" que foi realizado no repositório. Vamos consertar isso com um merge. Vamos juntar o ramobugFix
nomaster
:
$ git merge bugFix
- Uau! Viu isso? Antes de tudo, o
master
agora aponta para umcommit
que possui dois pais. Se você seguir as setas subindo a árvore decommits
a partir domaster
, você será capaz de encontrar, ao longo do caminho até a raiz, qualquer um doscommits
. Isso significa que omaster
contém todo o trabalho realizado no repositório até o momento. - Vamos juntar o
master
nobugFix
:
$ git checkout bugFix; git merge master
- Como o
bugFix
é um ancestral domaster
, o git não teve trabalho nenhum; ele só precisou mover obugFix
para o mesmocommit
domaster
. Agora todos oscommits
possuem a mesma cor, o que significa que ambos os ramos contém todo o trabalho realizado no repositório! Eba!
git checkout -b bugFix; git commit; git checkout master; git commit; git merge bugFix
-
A segunda forma de combinar trabalho entre ramos é o
rebase
. Orebase
essencialmente pega um conjunto decommits
, "copia" os mesmos, e os despeja em outro lugar. Isso pode parecer confuso, mas a vantagem dorebase
é que ele pode ser usado para construir uma sequência mais bonita e linear decommits
. O registro decommits
(história do repositório) ficará muito mais limpa se for utilizado apenasrebase
em vez demerge
. -
Abaixo temos dois ramos novamente; note que o ramo
bugFix
está atualmente ativo (veja o asterisco) -
Queremos mover nosso trabalho do
bugFix
diretamente dentro domaster
. Desta forma, vai parecer que esses dois recursos foram desenvolvidos sequencialmente, quando na realidade foram feitos em paralelo. Vamos fazê-lo com o comandogit rebase
$ git rebase master
- Incrível! Agora o trabalho do nosso ramo
bugFix
está logo após o domaster
, e temos uma linda sequência linear decommits
. Perceba que o commitC3
ainda existe em algum lugar (ele está clareado na árvore), e que oC3
' é a "cópia" que rebaseamos nomaster
. O único problema é que omaster
não foi atualizado também, vamos fazê-lo agora... - Agora o ramo
master
está ativo. Vamos em frente, fazerrebase
nobugFix
...
$ git rebase bugFix
- Aí está! Como o
master
era um ancestral dobugFix
, o git simplesmente moveu a referência do ramomaster
para frente na história.
$ git checkout -b bugFix; git commit; git checkout master; git commit; git checkout bugFix; git rebase master
- Primeiro temos que conversar sobre a "cabeça" (
HEAD
).HEAD
é um nome simbólico para ocommit
atualmente ativo (que sofreu checkout por último). É essencialmente ocommit
sobre o qual você está trabalhando no momento. - O
HEAD
sempre aponta para ocommit
mais recentemente copiado sobre a árvore de trabalho (arquivos do projeto). A maioria dos comandos do git que realizam mudanças sobre a árvore de trabalho começarão mudando oHEAD
. - Normalmente o
HEAD
aponta para o nome de um ramo (por exemplo,bugFix
). Quando você commita, o status dobugFix
é alterado e essa mudança ocorre também sobre oHEAD
. Vejamos isto em ação. Aqui vamos mostrar oHEAD
antes e depois de um commit.
$ git checkout C1; git checkout master; git commit; git checkout C2
- Veja! O
HEAD
estava se escondendo ao lado do nossomaster
esse tempo todo. - Soltar o
HEAD
significa anexá-lo a um commit em vez de anexá-lo a um ramo. Antes do estado solto (detached
), é assim como se parece:
HEAD -> master -> C1
$ git checkout C1
HEAD -> C1
$ git checkout C4
- Mover-se pela árvore do Git especificando o hash do
commit
pode se tornar um pouco entediante. No mundo real, você não terá à sua disposição essa bonita visualização da árvore ao lado do seu terminal, então você terá de usar o comandogit log
para ver os hashes. - Além disso, os hashes são geralmente muito maiores no mundo real. Por exemplo, o hash do
commit
que introduziu o nível de exercícios anterior éfed2da64c0efc5293610bdd892f82a58e8cbc5d8
. Não é algo exatamente fácil de lembrar. - O que salva é que o Git é inteligente com os hashes. Ele só exige que você especifique a quantidade de caracteres do hash suficiente para identificar unicamente o
commit
. Então eu posso digitar apenasfed2
em vez da grande string acima. - Como eu disse, especificar commits pelo hash não é a sempre o mais conveniente, e é por isso que o Git suporta referências relativas. Elas são fantásticas!
- Com referências relativas, você pode começar a partir de um ponto fácil de lembrar (como o ramo
bugFix
ou oHEAD
) e referenciar a partir dali. - Commits relativos são poderosos, mas vamos introduzir apenas dois tipos simples aqui:
- Mover para cima um commit por vez com
^
; - Mover para cima um número de vezes com
~<num>
.
- Mover para cima um commit por vez com
- Vamos dar uma olhada no operador circunflexo (
^
) primeiro. Cada vez que você adicioná-lo a um nome de referência, você está dizendo ao Git para encontrar o pai docommit
especificado. Então, dizermaster^
é equivalente a "o primeiro pai do master".master^^
é o avô (ancestral de segunda geração) do master. Vamos fazer checkout docommit
logo acima domaster
:
$ git checkout master^
- Boom! Pronto. Muito mais fácil que digitar o hash do
commit
. - Você também pode usar o
HEAD
como parte de uma referência relativa. Vamos usar isso para nos mover para cima algumas vezes na árvore de commits.
$ git checkout C3; git checkout HEAD^; git checkout HEAD^; git checkout HEAD^
- Fácil! Podemos viajar para trás no tempo com
HEAD^
$ git checkout bugFix^
- Digamos que você queira se mover vários níveis para cima na árvore de commits. Pode ser entediante digitar
^
várias vezes, e por isso o Git possui também o operador til (~
). Um número pode ser passado (opcionalmente) após o operador til, especificando o número de ancestrais que você deseja subir. Vamos vê-lo em ação:
$ git checkout HEAD~4
- Boom! Tão conciso. Referências relativas são incríveis.
- Agora que você é um especialista em referências relativas, vamos usá-las de fato para alguma coisa. Uma das situações mais comuns na qual eu uso referências relativas é quando quero trocar ramos de lugar. Você pode redefinir diretamente o
commit
para o qual um ramo aponta com a opção-f
. Desta forma, o seguinte comando:
$ git branch -f master HEAD~3
- Move (à força) o ramo master 3 ancestrais acima do
HEAD
. - Aqui vamos nós! As referências relativas nos deram uma forma concisa de nos referirmos ao
C1
, e a movimentação de ramos (com-f
) nos deu uma forma de apontar rapidamente um ramo para esse local.
$ git checkout HEAD^; git branch -f master C6; git branch -f bugFix HEAD^
- Existem várias maneiras de reverter mudanças no Git. E assim como o ato de commitar, reverter mudanças no Git também tem um componente de baixo nível (a preparação, ou
staging
, de arquivos ou trechos de arquivos individuais) e um componente de alto nível (como as mudanças são, de fato, revertidas). Aqui vamos focar neste último ponto. - Há duas maneiras principais de desfazer mudanças no Git -- uma delas é usando
git reset
, e a outra é usandogit revert
. Vamos olhar cada uma delas.
$ git reset HEAD~1
- Legal! O Git simplesmente moveu a referência do ramo
master
de volta paraC1
; agora o nosso repositório local está em um estado como se oC2
nunca tivesse acontecido. - Embora o
reset
funcione muito bem em ramos locais no seu próprio computador, o método utilizado de "reescrever o histórico" não funciona com ramos remotos que outras pessoas estejam usando. Para reverter mudanças e conseguir compartilhar essas mudanças com os outros, precisamos usar ogit revert
. Vejamo-lo em ação
git revert HEAD
- Estranho, um novo commit surgiu abaixo do commit que queríamos reverter. Isso é porque o novo
commit C2'
introduz mudanças -- acontece que as mudanças que ele introduz revertem exatamente aquelas docommit C2
. - Com o
revert
, você pode fazerpush
das suas mudanças para compartilhá-las com os outros.
$ git reset HEAD^; git checkout pushed; git revert HEAD
- Por enquanto nós abordamos o básico do Git -- commitar, criar ramos, e mover-se pela árvore. Apenas esses conceitos já são suficientes para utilizar 90% do poder dos repositórios Git, e cobrem as principais necessidades dos desenvolvedores.
- Os 10% restantes, entretanto, podem ser extremamente úteis em fluxos de trabalho complexos (ou quando você estiver em uma enrascada). O próximo conceito que vamos abordar é "movendo trabalho por aí" -- em outras palavras, veremos as formas como o desenvolvedor pode dizer "eu quero este trabalho aqui, e aquele ali" de formas precisas, eloquentes e flexíveis.
- Isso pode parecer muito, mas os conceitos são simples.
- O primeiro comando desta série é o
git cherry-pick
. Ele é chamado da seguinte forma:
$ git cherry-pick <Commit1> <Commit2> <...>
- Trata-se de uma forma bastante direta de dizer que você gostaria de copiar uma série de commits abaixo do seu local atual (
HEAD
). Eu pessoalmente amo ocherry-pick
porque há muito pouca mágica envolvida e é fácil de entender o funcionamento.Vejamos uma demonstração! - Aqui está um repositório onde temos algum trabalho no ramo side que desejamos copiar para o
master
. Isso poderia ser obtido por meio de umrebase
(que já aprendemos), mas vamos ver como ocherry-pick
se sai.
$ git cherry-pick C2 C4
- É isso! Queríamos os commits
C2
eC4
e o git os inseriu logo abaixo de nós. Simples assim!
$ git cherry-pick C3 C4 C7
- O
cherry-pick
é ótimo quando você sabe de antemão quais commits você quer (e você sabe os hashes correspondentes) -- é difícil bater a simplicidade que ele oferece. - Mas e quando você não sabe quais commits você quer? Felizmente o git pode te ajudar nesta situação também! Podemos usar o
rebase
interativo para isso -- trata-se da melhor forma de rever uma série de commits sobre os quais você está prestes a fazer umrebase
. Mergulhemos nos detalhes... - O
rebase
interativo é simplesmente o comando rebase com a opção-i
. Se você incluir essa opção, o git abrirá uma interface para mostrar quais commits estão prestes a serem copiados abaixo do alvo dorebase
. Ele também mostra os hashes e as mensagens dos commits, o que é ótimo para ter noção do que é o que. - No git "de verdade", a interface nada mais é que um arquivo aberto em um editor de texto (por exemplo o
vim
). Para os nossos propósitos, eu montei uma pequena janela que se comporta da mesma forma. - Quando a janela de
rebase
interativo abrir, você pode fazer 3 coisas diferentes:- Você pode reordenar os commits simplesmente mudando sua ordem na interface (na nossa janela isso significa arrastar e soltar com o mouse).
- Você pode escolher simplesmente omitir alguns commits. Para isso, clique no botão pick -- deixar o pick desligado significa que você quer descartar o
commit
. - Por fim, você pode "esmagar" (fazer
squash
) nos commits. Infelizmente, nosso tutorial não será capaz de cobrir essa funcionalidade por alguns motivos logísticos, então vamos pular os detalhes disto. Em resumo, no entanto, osquash
permite que você combine commits.
- Ótimo! Vejamos um exemplo. Quando você clicar o botão, uma janela de
rebase
interativo se abrirá. Reordene alguns commits da forma como você preferir (ou sinta-se livre para desmarcar o pick de alguns) e veja o resultado!
$ git rebase -i HEAD~4
- Boom! O Git copiou alguns commits exatamente da mesma forma que você os especificou na janela
$ git rebase -i HEAD~4
- Aqui está uma situação de acontece frequentemente com desenvolvedores: Estou tentando encontrar um bug, mas ele é escorregadio. Para auxiliar meu trabalho de detetive, eu coloco alguns comandos de debug e prints.
- Todos esses comandos de debug e mensagens estão em seus próprios ramos. Finalmente eu encontro o bug, corrijo, e me regozijo!
- O único problema é que agora eu preciso devolver o meu
bugFix
ao ramomaster
. Se eu simplesmente der um fast-forward nomaster
, então omaster
terminará contendo todos os comandos de debug, o que é indesejável. Deve existir alguma outra forma... - Precisamos dizer ao git para copiar somente um dos commits. Esta situação é exatamente a mesma dos níveis anteriores a respeito de como mover trabalho -- podemos usar os mesmos comandos:
git rebase -i
git cherry-pick
- Para alcançar o objetivo.
$ git rebase -i HEAD~3; git rebase bugFix master
- Aqui está outra situação que acontece com bastante frequência. Você fez algumas mudanças (
newImage
), além de um outro conjunto de mudanças (caption
) que são relacionadas, de forma que elas estão empilhadas uma após a outra no seu repositório. - O complicado é que algumas vezes você precisa fazer uma pequena modificação em um
commit
mais antigo. Neste caso, o pessoal do design quer que modifiquemos um pouco as dimensões da imagem introduzida emnewImage
, apesar de esse commit estar mais para trás no nosso histórico!! - Superaremos essa dificuldade fazendo o seguinte:
- Reordenaremos os commits de forma que aquele que desejamos esteja no topo, com
git rebase -i
; - Usaremos o comando
git commit --amend
para fazer uma pequena modificação; - Vamos, então, reordenar os commits na mesma ordem que estavam anteriormente com
git rebase -i
; - Finalmente, moveremos o
master
para essa parte atualizada da árvore para finalizar o nível (usando o método de sua escolha).
- Reordenaremos os commits de forma que aquele que desejamos esteja no topo, com
- Há muitas formas de alcançar o objetivo final (eu vejo o
cherry-pick
passando pela sua mente), e veremos mais delas depois, mas por enquanto foquemos nesta técnica. - Por último, preste atenção no estado do "objetivo" aqui -- como nós movemos os commits duas vezes, ambos ficam com um apóstrofo. Um apóstrofo adicional é colocado no
commit
que sofreu oamend
, o que nos dá a forma final da árvore. - Tendo dito isto, posso avaliar a resposta baseado na estrutura e nas diferenças relativas de número de apóstrofos. Desde que o ramo
master
da sua árvore tenha a mesma estrutura, e o número de apóstrofos seja igual a menos de uma constante, darei a você todos os pontos para esta tarefa.
$ git rebase -i HEAD~2; git commit --amend; git rebase -i HEAD~2; git rebase caption master
- Como você viu no nível anterior, usamos
rebase -i
para reordenar os commits. Uma vez que ocommit
que queríamos mudar estava no topo, pudemos facilmente usar o--amend
e depois reordená-lo de volta para obter nossa ordem preferida. - O único problema aqui é que há muita reordenação ocorrendo, o que pode introduzir conflitos de
rebase
. Vamos dar uma olhada em outro método, usando ogit cherry-pick
. - Lembre-se que o
git cherry-pick
copiará umcommit
de qualquer lugar na árvore sob oHEAD
(desde que essecommit
não seja um ancestral doHEAD
). - Aqui está uma demonstração para refrescar sua memória:
$ git cherry-pick C2
- Ótimo! Vamos em frente.
- Então, neste nível, vamos alcançar o mesmo objetivo de fazer "amend" no
C2
, mas evitaremos usar orebase -i
. Agora vou deixar com você a tarefa de descobrir como fazer! - Lembre-se, o número exato de apóstrofos (') nos commits não é importante, apenas as diferenças relativas.
$ git checkout master; git cherry-pick C2; git commit --amend; git cherry-pick C3
- Como você aprendeu nas lições anteriores, ramos são fáceis de mover e geralmente vão se referindo a diferentes commits conforme você vai trabalhando no código. Ramos são facilmente mutáveis, frequentemente temporários, e estão sempre mudando.
- Se este é o caso, você pode estar se perguntando se não existe uma forma de marcar permanentemente pontos históricos do projeto. Para coisas como grandes releases ou grandes merges, existe alguma forma de marcar commits com algo mais permanente que um ramo?
- Você acertou a aposta, existe sim! As tags do Git foram criadas exatamente para esse caso de uso -- elas marcam de forma (relativamente) permanente certos commits como se fossem "pedras de kilometragem" ("milestones") em uma estrada, e você pode referenciá-las exatamente como faz com ramos.
- O mais importante, no entanto, é que elas nunca se movem sozinhas quando novos commits são criados. Você pode fazer
checkout
em umatag
e então completar trabalho nessatag
-- tags existem como âncoras na árvore de commits que estão atreladas a certos pontos. - Vejamos como as tags se comportam na prática. Criemos uma
tag
emC1
, que é nosso protótipo da versão 1:
$ git tag v1 C1
- Aqui! Bem fácil. Nós chamamos a
tag
dev1
e referenciamos o commitC1
explicitamente. Se você chamar o comando sem especificar umcommit
, o git vai usar seja lá qualcommit
para o qual oHEAD
estiver apontando.
$ git tag v0 C1; git tag v1 C2; git checkout C2
- Devido ao fato de as tags servirem como "âncoras" tão boas no código, o Git tem um comando para descrever onde você está com relação à "âncora" (
tag
) mais próxima. Esse comando é chamadogit describe
! - O
git describe
pode ajudar a recuperar a sua orientação depois de você ter se movido muitos commits para trás ou para frente no histórico; isso pode acontecer depois de você completar umgit bisect
(uma busca para debug) ou quando se sentar no computador de um colega que acabou de voltar de férias. O git describe
é chamado da seguinte forma:
$ git describe <ref>
- Onde
<ref>
é qualquer coisa que o git possa resolver como uma referência a umcommit
. Se você não especificar o ref, o Git usa simplesmente ocommit
atual (HEAD
). A saída do comando é mais ou menos assim:
<tag>_<numCommits>_g<hash>
- Onde
tag
é atag
ancestral mais próxima no histórico,numCommits
é o número de commits de distância datag
, e<hash>
é o hash docommit
sendo descrito. Vejamos um exemplo rápido. Para a árvore abaixo:
$ git tag v2 C3
- O comando
git describe master
daria a saída:
v1_2_gC2
- Enquanto
git describe side
daria:
v2_1_gC4
- Cara, temos um monte de ramos aqui! Vamos fazer um rebase de todo o trabalho contido nesses ramos para o master.
- No entanto, a cúpula da administração está tornando as coisas mais difíceis -- eles querem que os commits estejam todos em ordem sequencial. Isso significa que a nossa árvore final precisa ter o
C7'
por último,C6'
acima disso, e assim por diante, tudo ordenado. - Se você fizer besteira, sinta-se livre para usar o comando reset para recomeçar do zero. Depois lembre de olhar nossa solução do gabarito para ver se consegue resolver a tarefa usando menos comandos
$ git rebase master bugFix; git rebase bugFix side; git rebase side another; git rebase another master
- Assim como o modificador
~
, o modificador^
também aceita um número opcional depois dele. - Em vez de especificar o número de gerações a voltar (que é o que o
~
faz), o modificador no^
especifica qual referência de pai a ser seguida a partir de um commit de merge. Lembre-se que commits de merge possuem múltiplos pais, então o caminho a seguir é ambíguo. - O Git normalmente subirá o "primeiro" pai de um commit de merge, mas especificar um número após o
^
muda esse comportamento padrão. - Basta de conversa, vejamos o operador em ação.
- Aqui temos um commit de merge. Se fizermos checkout em
master^
sem especificar um número, vamos seguir o primeiro pai acima do commit de merge (Em nossa visualização, o primeiro pai é aquele diretamente acima do commit de merge).
$ git checkout master^
- Fácil -- isso é aquilo com o que já estamos acostumados.
- Agora vamos, em vez disso, especificar o segundo pai...
$ git checkout master^2
- Viu? Subimos para o outro pai.
- Os modificadores
^
e~
podem tornar a movimentação ao redor da árvore de commits muito poderosa:
$ git checkout HEAD~; git checkout HEAD^2; git checkout HEAD~2
$ git checkout HEAD~^2~2
- O mesmo movimento que o anterior, mas tudo em um único comando.
$ git branch bugWork master^^2^
- Uhuuuuu Nelly! Temos um belo de um objetivo para alcançar neste nível. Temos aqui um master que está alguns commits atrás dos ramos one, two e three. Seja lá por qual razão, precisamos atualizar esses três outros ramos com versões modificadas dos últimos commits do master.
- O ramo one precisa de uma reordenação e da exclusão do C5. O two precisa apenas de reordenação. O three precisa de um único commit!
- Vamos deixar você descobrir como resolver esta tarefa -- mas não deixe de ver a nossa solução depois com o comando show solution.
$ git checkout one; git cherry-pick C4 C3 C2; git checkout two; git cherry-pick C5 C4 C3 C2; git rebase C2 three