Aprenda conceitos de git, não comandos
Esse treinamento é quase de graça, basta deixar uma star Um tutorial interativo de git, destinado a ensinar como o git funciona, não apenas quais comandos executar.
Então você quer usar o git, certo?
Mas você não quer apenas aprender comandos, quer entender o que está usando?
Então isso é para você!
Vamos começar!
Esse treinamento é uma tradução e adaptação do excelente conteúdo Learn git concepts, not commands, de Nicola Riedmann. Thanks Nico
😊
Com base no conceito geral da postagem do blog de Rachel M. Carmena em How to teach Git.
Embora eu ache muitos tutoriais de git na internet focados no que fazer, ao invés de como as coisas funcionam, o recurso mais inestimável para ambos (e a fonte para este tutorial!) é o Pro Git Book (traduzido para PT-BR) e a página de referência.
Então, se você ainda estiver interessado quando terminar aqui, vá conferir! Espero que o conceito um pouco diferente deste tutorial o ajude a entender todos os outros recursos git detalhados lá.
- Visão geral
- Obtendo um Remote Repository
- Adicionando coisas novas
- Fazendo mudanças
- Ramificação (Branch)
- Mesclagem (Merging)
- Rebasing
- Atualizando o Dev Environment com as alterações remotas
- Cherry-picking
- Reescrevendo a história
- Lendo a história
Visão geral
Na imagem abaixo existem 4 caixas. Uma delas fica sozinha, enquanto as outras três estão agrupadas no que chamarei de Development Environment.
Vamos começar com o que está sozinho. O Remote Repository é para onde você envia as alterações quando deseja compartilhá-las com outras pessoas e de onde obtém as alterações. Se você já usou outros sistemas de controle de versão, não há nada de interessante nisso.
O Development Environment é o que você possui na sua máquina local. As três partes são seu Working Directory, a Staging Area e o Local Repository. Aprenderemos mais sobre eles quando começarmos a usar o git
Escolha um local em que você deseja colocar seu Development Environment. Basta ir para a sua pasta pessoal ou para onde você quiser colocar seus projetos. Você não precisa criar uma nova pasta para o seu Dev Environment.
Obtendo um Remote Repository
Agora queremos pegar um Remote Repository e colocar o que está nele na sua máquina.
Eu sugiro que usemos o repositório github.com/PauloGoncalvesBH/treinamento-git no nosso treinamento.
Para fazer isso use o comando
git clone https://github.com/PauloGoncalvesBH/treinamento-git.git
Mas, ao seguir este tutorial, você precisará enviar as alterações feitas no seu Dev Environment de volta ao Remote Repository, e o github não permite que uma pessoa faça isso no repositório de outra pessoa, por isso o melhor a fazer é criar um fork. Há um botão para fazer isso no canto superior direito desta página.
Agora que você já possui uma cópia do meu Remote Repository na sua conta do github por ter feito o fork, é hora de colocar isso em sua máquina.
Para isso usamos git clone https://github.com/{SEU USUÁRIO}/treinamento-git.git
Como você pode ver no diagrama abaixo, isso copia o Remote Repository em dois lugares, seu Working Directory e o Local Repository. Agora você vê como o git é um controle de versão distribuído. O Local Repository é uma cópia do Remote e age exatamente como ele. A única diferença é que você não o compartilha com ninguém.
O que o git clone
também faz é criar uma nova pasta no local aonde você executou o comando. Deve haver uma pasta treinamento-git
agora. Abra-a.
Adicionando coisas novas
Alguém já colocou um arquivo chamado Alice.txt
no Remote Repository. É meio solitário lá, então vamos criar um novo arquivo e chamá-lo de Bob.txt
.
O que você acabou de fazer é adicionar um arquivo no seu Working Directory. Existem dois tipos de arquivos no seu Working Directory: Arquivos tracked, que o git conhece, e untracked, arquivos que o git (ainda) não conhece.
Para ver o que está acontecendo no seu Working Directory execute git status
, que informará em que branch você está, se o seu Local Repository é diferente do Remote e os arquivos tracked e untracked.
Você verá que Bob.txt
não é rastreado (untracked) e o git status
até lhe diz como mudar isso.
Na figura abaixo, você pode ver o que acontece quando você segue a dica e executa git add Bob.txt
: Você adicionou o arquivo à Staging Area, onde você coleta todas as alterações que deseja incluir no Repository.
Quando você adicionar todas as suas alterações (que agora é apenas adicionar Bob.txt
), você estará pronto para fazer o commit do que acabou de fazer no Local Repository.
As alterações que você fez são uma parte significativa do trabalho, portanto, quando você executa o git commit
, um editor de texto será aberto e permitirá que você escreva uma mensagem dizendo tudo o que você acabou de fazer. Quando você salva e fecha o arquivo de mensagens, seu commit é adicionado ao Local Repository.
Você também pode adicionar sua mensagem de commit na linha de comando se você chamar git commit
assim: git commit -m "Adicionar Bob"
. Mas como você deseja escrever boas mensagens de commit, você deve gastar um tempo estudando e usar o editor.
Agora suas alterações estão no seu repositório local, o que é um bom local para elas, desde que ninguém mais precise delas ou você ainda não esteja pronto para compartilhá-las.
Para compartilhar seus commits com o Remote Repository você precisa empurrá-los (push
).
Depois de executar o comando git push
as alterações serão enviadas para o Remote Repository. No diagrama abaixo, você vê o estado após o seu push
.
Fazendo mudanças
Até agora apenas adicionamos um novo arquivo. Obviamente a parte mais interessante do controle de versão é a alteração de arquivos.
Dê uma olhada no arquivo Alice.txt
.
Na verdade ele contém algum texto, mas Bob.txt
não, então vamos mudar isso e colocar Oi!! Eu sou o Bob. Eu sou novo aqui.
.
Se você executar o git status
agora, verá que o Bob.txt
está modificado (modified
).
Nesse estado as alterações estão apenas no seu Working Directory.
Se você deseja ver o que mudou no seu Working Directory, você pode executar o git diff
e ver a seguinte saída:
diff --git a/Bob.txt b/Bob.txt
index e69de29..3ed0e1b 100644
--- a/Bob.txt
+++ b/Bob.txt
@@ -0,0 +1 @@
+Oi!! Eu sou o Bob. Eu sou novo aqui.
Vá em frente e execute git add Bob.txt
como você fez anteriormente. Como sabemos, isso move suas alterações para a Staging Area.
Eu quero ver as mudanças que acabamos de realizar, então vamos executar git diff
novamente! Você notará que desta vez a saída está em branco. Isso acontece porque o git diff
opera apenas nas alterações no seu Working Directory.
Para mostrar quais mudanças já estão na Staging Area, podemos executar git diff --staged
e veremos a mesma saída diff de antes.
Acabei de notar que colocamos dois pontos de exclamação após o 'Oi'. Eu não gosto disso, então vamos mudar o Bob.txt
novamente, para que seja apenas 'Oi!'
Se agora rodarmos git status
, veremos que existem duas mudanças: A que já enviamos para a Staging Area, onde adicionamos texto, e a que acabamos de fazer, que ainda está apenas no diretório de trabalho.
Podemos dar uma olhada no git diff
entre o Working Directory e o que já enviamos para a Staging Area, para mostrar o que mudou desde que nos sentimos prontos para realizar um commit das mudanças.
diff --git a/Bob.txt b/Bob.txt
index 8eb57c4..3ed0e1b 100644
--- a/Bob.txt
+++ b/Bob.txt
@@ -1 +1 @@
-Oi!! Eu sou o Bob. Eu sou novo aqui.
+Oi! Eu sou o Bob. Eu sou novo aqui.
Como a mudança é o que queríamos, vamos executar git add Bob.txt
para enviar o estado atual do arquivo para stage.
Agora estamos prontos para realizar o commit
com o que acabamos de fazer. Eu criei o commit com git commit -m "Alterar texto de Bob"
porque senti que, para uma mudança tão pequena, escrever uma linha seria suficiente.
Como sabemos, as alterações estão agora no Local Repository. Ainda podemos querer saber que mudança acabamos de commitar e o que havia antes.
Podemos fazer isso comparando commits. Todo commit no git tem um hash exclusivo pelo qual é referenciado.
Se dermos uma olhada no git log
, não apenas veremos uma lista de todos os commits com hash, como Autor e Data, também veremos o estado do nosso Local Repository e as informações locais mais recentes sobre branches remotas .
No momento, o git log
se parece com isso:
commit 87a4ad48d55e5280aa608cd79e8bce5e13f318dc (HEAD -> master)
Author: {VOCÊ} <{SEU EMAIL}>
Date: Sun Jan 27 14:02:48 2019 -0300
Alterar texto de Bob
commit 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e (origin/master, origin/HEAD)
Author: {VOCÊ} <{SEU EMAIL}>
Date: Sun Jan 27 13:35:41 2019 -0300
Adicionar Bob
commit 71a6a9b299b21e68f9b0c61247379432a0b6007c \1
Author: Paulo Gonçalves <[email protected]>
Date: Fri Jan 25 20:06:57 2019 -0300
Adicionar Alice
commit ddb869a0c154f6798f0caae567074aecdfa58c46
Author: Paulo Gonçalves <[email protected]>
Date: Fri Jan 25 19:25:23 2019 -0300
Adicionar texto do tutorial
Todas as alterações no tutorial são compactadas neste commit para manter o log livre de desorganização que o distrai.
Aqui vemos algumas coisas interessantes:
- Os dois primeiros commits são feitos por mim.
- Seu commit inicial para adicionar Bob é o HEAD atual da branch master no Remote Repository. Veremos isso novamente quando falarmos sobre ramificações (branches) e obter alterações remotas.
- O último commit no Local Repository é o que acabamos de fazer e agora sabemos o seu hash.
Observe que os hashes dos commits serão diferentes para você. Se você quiser saber exatamente como o git chega a esses IDs de revisão, dê uma olhada neste artigo sobre a anatomia de um commit.
Para comparar esse commit e o anterior, podemos utilizar git diff <commit>^!
(Onde ^!
diz ao git para comparar o commit com o que veio antes dele). Portanto, neste caso, eu executo git diff 87a4ad48d55e5280aa608cd79e8bce5e13f318dc^!
.
Também podemos fazer o git diff 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e 87a4ad48d55e5280aa608cd79e8bce5e13f318dc
para o mesmo resultado e, em geral, comparar quaisquer dois commits. Note que o formato aqui é git diff <de commit> <para commit>
, então nosso novo commit fica em segundo.
No diagrama abaixo você vê novamente os diferentes estágios de uma alteração e os comandos diff correspondentes.
Agora que temos certeza de que fizemos a alteração que queríamos, vá em frente e execute git push
.
Ramificação (Branch)
Outra coisa que torna o git excelente é o fato de que trabalhar com ramificações é realmente uma parte fácil e essencial de como você trabalha com o git.
De fato, trabalhamos em uma branch desde que começamos.
Quando você clona o Remote Repository, seu Dev Environment inicia automaticamente na ramificação principal do repositório, ou seja, master.
Há um movimento atual para a branch principal deixar de ser chamada como master e passar a ser trunk ou main. Linux, Github e outras companhias estão adotando a nova nomenclatura. É uma ótima proposta e totalmente alinhada ao movimento
#BlackLivesMatter
. Você pode entender mais lendo o artigo The bigger picture behind the GitHub master branch name change.
A maioria dos fluxos de trabalho com o git incluem fazer suas alterações em uma branch antes de você mesclá-las (merge
) novamente na master.
Normalmente você estará trabalhando por conta própria até que esteja pronto e confiante das suas alterações, que poderão ser mescladas (mergeadas) na master.
Muitos gerenciadores de repositório git, como o GitLab e o GitHub, permitem que as branches sejam protegidas, o que significa que nem todo mundo pode simplesmente empurrar (
push
) as mudanças pra lá. O master geralmente é protegido por padrão.
Não se preocupe, retornaremos a todas essas coisas com mais detalhes quando precisarmos delas.
No momento queremos criar uma branch para fazermos algumas alterações. Talvez você queira apenas tentar algo por conta própria e não mexer com o estado de trabalho na sua branch master, ou não pode empurrar (push
) para a master.
As branches ficam no Local e no Remote Repository. Quando você cria uma nova branch, o conteúdo dessa branch será uma cópia de qualquer ramificação em que você esteja trabalhando no momento.
Vamos fazer algumas alterações no Alice.txt
! Que tal colocarmos algum texto na segunda linha?
Queremos compartilhar essa mudança, mas não colocá-la na master imediatamente, então vamos criar uma ramificação para ela usando git branch <branch name>
.
Para criar uma nova branch chamada change_alice
, você pode executar o comando git branch change_alice
.
Isso adiciona a nova branch ao Local Repository.
Enquanto seu Working Directory e Staging Area realmente não se importam com branches, você sempre cria commit
com a branch em que está atualmente.
Você pode pensar em branches no git como ponteiros, apontando para uma série de confirmações. Quando você faz um commit
, você adiciona o que você está apontando no momento.
Apenas adicionar uma branch não o leva diretamente para lá, apenas cria um ponteiro. De fato, o estado em que seu Local Repository está atualmente, pode ser visto como outro ponteiro, chamado HEAD, que aponta para qual branch e commit você está atualmente.
Se isso parecer complicado, os diagramas abaixo ajudarão a esclarecer um pouco as coisas:
Para mudar para a nossa nova branch, você terá que usar o comando git checkout change_alice
. O que isso faz é simplesmente mover o HEAD para a branch que você especificar.
Como você normalmente deseja mudar para uma branch logo após criá-la, existe a conveniente opção
-b
disponível para o comandocheckout
, que permite realizarcheckout
diretamente em uma branch nova, para que você não precisa criá-la de antemão.Então, para criar e mudar para a nossa branch
change_alice
, também poderíamos ter executadogit checkout -b change_alice
. Mais simples, não?
Você notará que seu Working Directory não mudou e o fato de termos modificado Alice.txt
ainda não está relacionado à branch em que estamos inseridos. Agora você pode adicionar (add
) e fazer commit
da alteração em Alice.txt
, como fizemos no master antes, que irá mover o arquivo para a Staging Area (nesse ponto ainda não está relacionado à branch) e, finalmente, 'committar' sua alteração na branch change_alice
.
Há apenas uma coisa que você não pode fazer ainda. Tente enviar (git push
) suas alterações para o Remote Repository.
Você verá o seguinte erro e - como o git está sempre pronto para ajudar - uma sugestão de como resolver o problema:
fatal: The current branch change_alice has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin change_alice
Mas não queremos fazer isso às cegas. Estamos aqui para entender o que realmente está acontecendo. Então, o que são upstream branches e remotes?
Lembra quando clonamos o Remote Repository há um tempo atrás? Nesse ponto, ele não continha apenas este tutorial e Alice.txt
, mas na verdade duas ramificações.
Quando copiamos as coisas no Remote Repository para o seu Dev Environment, algumas etapas extras ocorreram embaixo do capô.
O Git configurou o remote do seu Local Repository para ser o Remote Repository que você clonou e deu a ele o nome padrão origin
.
Seu Local Repository pode rastrear vários remotes e eles podem ter nomes diferentes, mas seguiremos apenas com a
origin
nesse tutorial.
Em seguida, ele copiou as duas branches remotas no seu Local Repository e, finalmente, alterou para a master para você (git checkout
).
Ao fazer isso, outra etapa implícita acontece. Quando você faz checkout
de um nome de uma branch que tenha uma correspondência exata nas branches remotas, você obterá uma nova branch local que está vinculada à branch remote. A branch remote é a branch upstream do seu local.
Nos diagramas anteriores, você pode ver apenas as branches locais que possui. Você pode ver essa lista de branches locais executando o comando git branch
.
Se você quiser ver também as branches remotas que seu Local Repository conhece, você pode executar git branch -a
para listar todas elas.
Agora podemos executar o comando sugerido git push --set-upstream origin change_alice
, e empurrar (push
) as alterações em nossa branch para um novo remote. Isso criará a branch change_alice
no Remote Repository e definirá o nosso local change_alice
para rastrear essa nova branch.
Existe outra opção se realmente queremos que nosso ramo rastreie algo que já existe no Remote Repository. Talvez um colega já tenha promovido algumas mudanças, enquanto estávamos trabalhando em alguma questão relacionada em nossa branch local, e gostaríamos de integrar as duas. Então poderíamos simplesmente definir o upstream para a nossa branch
change_alice
como um novo remote usandogit branch --set-upstream-to=origin/change_alice
e daí para rastrear a branch remote.
Depois disso, dê uma olhada no seu Remote Repository no github, sua branch estará lá, pronto para outras pessoas verem e trabalharem.
Vamos ver como você pode obter as alterações de outras pessoas em seu Dev Environment em breve, mas primeiro trabalharemos um pouco mais com as branches, para introduzir todos os conceitos que também entram em jogo quando obtemos novidades do Remote Repository.
Mesclagem (Merging)
Como você e todo mundo em geral trabalharão em branches, precisamos conversar sobre como obter alterações de uma branch para outra, mergeando elas.
Acabamos de alterar o arquivo Alice.txt
na branch change_alice
, e eu diria que estamos felizes com as alterações que fizemos.
Se você executar git checkout master
, o commit
que fizemos na outra branch não estará lá. Para colocar as alterações na master, precisamos mesclar (merge
) a branch change_alice
na master.
Note que você sempre mescla uma branch específica com a que você está atualmente.
Merge Fast-Forward
Como já fizemos o checkout na master, agora podemos executar git merge change_alice
.
Como não existem outras alterações conflitando em Alice.txt
e não mudamos nada na master, isso ocorrerá sem problemas na chamada 'fusão' fast forward (rápida).
Nos diagramas abaixo, você pode ver que isso significa apenas que o ponteiro master pode simplesmente ser avançado para onde o change_alice está.
O primeiro diagrama mostra o estado antes de nossa mesclagem (merge
). A master ainda está no commit que era originalmente e, na outra branch, fizemos mais um commit.
O segundo diagrama mostra o que mudou com o nosso merge
.
Mesclando branches divergentes
Vamos tentar algo mais complexo.
Adicione algum texto em uma nova linha em Bob.txt
na master e faça um commit.
Então execute git checkout change_alice
, altere Alice.txt
e commite.
No diagrama abaixo, você vê como nosso histórico de commits agora se parece. Master e change_alice
se originaram do mesmo commit, mas desde então eles divergiram, cada um tendo seu próprio commit adicional.
Se você voltar para a master (git checkout master
) e executar git merge change_alice
, uma mesclagem de avanço rápido (fast forward) não será possível. Em vez disso, seu editor de texto favorito será aberto e permitirá que você altere a mensagem do commit de merge que o git está prestes a criar para reunir as duas branches. Você pode apenas seguir com a mensagem padrão. O diagrama abaixo mostra o estado de nossa história do git após a mesclagem (merge
).
O novo commit envia as alterações que fizemos na branch change_alice
para a master.
Como você deve se lembrar, as revisões no git não são apenas uma captura instantânea de seus arquivos, mas também contêm informações de onde elas vieram. Cada commit
tem um ou mais commits pais. Nosso novo commit de merge
possui como seus pais o último commit da master e o commit que fizemos na branch change_alice
.
Resolvendo conflitos
Até agora nossas mudanças não interferiram entre si.
Vamos criar um conflito e depois solucioná-lo.
Crie e faça o checkout de uma nova branch. Você sabe como, mas talvez tente usar o git checkout -b
para facilitar sua vida.
Eu chamei a minha de branch_do_bobby
.
Nessa branch faremos uma alteração no Bob.txt
.
A primeira linha ainda deve ser Oi! Eu sou o Bob. Eu sou novo aqui.
Mude isso para Oi! Eu sou o Bobby. Eu sou novo aqui.
Faça o commit
da sua alteração e volte (checkout
) para a branch master. Aqui vamos mudar a mesma linha para Oi!! Eu sou o Bob. Estou aqui há um tempo.
e realizar um commit
da alteração.
Agora é hora de fazer o merge
da branch branch_do_bobby
com a master.
Ao tentar isso, você verá a seguinte mensagem:
Auto-merging Bob.txt
CONFLICT (content): Merge conflict in Bob.txt
Automatic merge failed; fix conflicts and then commit the result.
A mesma linha mudou nas duas branches, e o git não pode lidar com isso sozinho.
Se você executar git status
, receberá todas as instruções úteis de como continuar.
Primeiro, temos que resolver o conflito manualmente.
Para um conflito fácil como este seu editor de texto favorito se sairá bem. Para mesclar arquivos grandes com muitas alterações, uma ferramenta mais poderosa tornará sua vida muito mais fácil, e eu suponho que sua IDE favorita venha com ferramentas de controle de versão e uma bela visualização para mesclagem.
Se você abrir Bob.txt
, verá algo semelhante a isso (eu trunquei o que quer que tenhamos colocado na segunda linha antes):
<<<<<<< HEAD
Oi!! Eu sou o Bob. Estou aqui há um tempo.
=======
Oi! Eu sou o Bobby. Eu sou novo aqui.
>>>>>>> branch_do_bobby
[... tanto faz o que você colocou na linha 2]
No topo, você vê o que mudou em Bob.txt
no HEAD atual. Abaixo, o que mudou na branch que estamos mesclando.
Para resolver o conflito manualmente, você só precisa ter um conteúdo razoável e sem as linhas especiais que o git introduziu no arquivo.
Então vá em frente e mude o arquivo para algo assim:
Oi! Eu sou o Bobby. Estou aqui há um tempo.
[...]
A partir daqui, o que estamos fazendo é exatamente o que faríamos para qualquer alteração.
Nós enviamos para stage quando executamos add Bob.txt
, e então fazemos o commit
.
Já conhecemos o commit das alterações que fizemos para resolver o conflito. É o merge commit que está sempre presente ao mesclar.
Se alguma vez você perceber no meio da resolução de conflitos que realmente não deseja seguir com o merge
, você pode simplesmente cancelar (abort
) executando o comando git merge --abort
.
Rebasing
Git tem outra maneira limpa de integrar mudanças entre duas branches, que é chamada de rebase
.
Ainda lembramos que uma branch é sempre baseada em outra. Quando você a cria, você ramifica de algum lugar.
No nosso exemplo de mesclagem simples, ramificamos a master em um commit específico e, em seguida, fizemos commit de algumas mudanças no master e na branch change_alice
.
Quando uma ramificação está divergindo daquela em que se baseia e você deseja integrar as alterações mais recentes em sua ramificação atual, o rebase
oferece uma maneira mais limpa de fazer isso do que uma mesclagem (merge
) faria.
Como vimos, um merge
introduz um merge commit no qual os dois históricos são integrados novamente.
Visto de forma simples, o rebasing muda apenas o ponto da história (o commit) no qual sua ramificação se baseia.
Para tentar isso, vamos primeiro fazer o checkout da branch master novamente e depois criar uma nova branch baseada nela.
Chamei a minha branch de add_patrick
, adicionei um novo arquivo chamado Patrick.txt
e fiz um commit com a mensagem 'Adicionar Patrick'.
Após o commit, volte para a master, faça uma alteração e faça o commit. Eu adicionei mais algum texto em Alice.txt
.
Como em nosso exemplo de mesclagem, a história dessas duas branches divergem em um ancestral comum, como você pode ver no diagrama abaixo.
Agora vamos executar checkout add_patrick
novamente, pegar a mudança que foi feita na master e enviar para a branch em que estamos trabalhando!
Quando executamos git rebase master
, baseamos nossa ramificação add_patrick
no estado atual da branch master.
A saída desse comando nos dá uma boa dica do que está acontecendo:
First, rewinding head to replay your work on top of it...
Applying: Adicionar Patrick
Como lembramos, HEAD é o ponteiro para o commit atual em que estamos em nosso Dev Environment.
Está apontando para o mesmo lugar que add_patrick
antes do rebase começar. Para o rebase, ele volta primeiro ao ancestral comum, antes de passar para o head atual da branch que queremos basear.
Portanto, o HEAD passa do commit 0cfc1d2 para o commit 7639f4b que está no head da master.
Então rebase aplica cada commit que fizemos na nossa branch add_patrick
.
Para ser mais exato o que o git faz depois de retornar o HEAD de volta ao ancestral comum dos branches, é armazenar partes de cada commit que você fez na branch (o diff
das mudanças, o texto do commit, autor, etc. .).
Depois disso, ele faz um checkout
do último commit da branch na qual você está reestruturando e, em seguida, aplica cada uma das alterações armazenadas como um novo commit no topo dela.
Portanto, em nossa visão simplificada original, assumiríamos que após o rebase
o commit 0cfc1d2 não aponta mais para o ancestral comum em sua história, mas aponta para o head da master.
De fato, o commit 0cfc1d2 não existe mais, e a branch add_patrick
começa com um novo commit 0ccaba8, que tem o commit mais recente de master como seu ancestral.
Nós fizemos parecer que a branch add_patrick
foi baseada na master atual, e não em uma versão mais antiga, mas ao fazê-lo reescrevemos o histórico da branch.
No final deste tutorial, aprenderemos um pouco mais sobre como reescrever o histórico e quando é apropriado e inapropriado fazê-lo.
Rebase
é uma ferramenta incrivelmente poderosa quando você está trabalhando em sua própria branch de desenvolvimento, que é baseada em uma branch compartilhada, por exemplo, a master.
Usando o rebase, você pode garantir que integra frequentemente as alterações que outras pessoas fazem e enviam para master, mantendo um histórico linear limpo que permite fazer uma mesclagem de avanço rápido (fast-forward merge
) quando chegar a hora de colocar seu trabalho na branch compartilhada.
Manter um histórico linear também torna a leitura ou a visualização (tente git log --graph
ou dê uma olhada na visualização de branch do GitHub ou GitLab) do log de commits muito mais agradável do que ter um histórico repleto de merge commits, geralmente usando o texto padrão.
Resolvendo conflitos
Assim como em um merge
, você pode ter conflitos se você tiver dois commits alterando as mesmas partes de um arquivo.
No entanto, quando você encontra um conflito durante um rebase
você não o corrige em um merge commit extra, mas pode simplesmente resolvê-lo no commit que está sendo aplicado no momento.
Novamente, baseando suas alterações diretamente no estado atual da branch original.
Resolver conflitos de rebase
é muito parecido com o que você faria para um merge
, portanto, consulte a seção se você não tiver mais certeza de como fazê-lo.
A única distinção é que, como você não está introduzindo um merge commit, não há necessidade de fazer um commit
da sua resolução. Simplesmente execute add
das alterações, enviando para Staging Environment, e depois execute git rebase --continue
. O conflito será resolvido no commit que estava sendo aplicado.
Assim como no merge, você sempre pode parar e cancelar tudo o que fez até o momento executando git rebase --abort
.
Atualizando o Dev Environment com as alterações remotas
Até agora, aprendemos apenas como fazer e compartilhar alterações.
Isso se encaixa no que você fará se estiver trabalhando por conta própria, mas geralmente haverá muitas pessoas que fazem o mesmo e queremos que as alterações sejam alteradas do Remote Repository para o nosso Dev Environment de alguma forma.
Como já faz algum tempo, vamos dar uma outra olhada nos componentes do git:
Assim como o seu Dev Environment, todos os outros que trabalham no mesmo código-fonte possui o seu próprio.
Todos esses Dev Environments possuem suas próprias alterações em Working directory e Staging Area, que em algum momento geram um novo commit
no Local Repository e são finalmente empurradas (push
) para o Remote Repository.
No nosso exemplo, usaremos as ferramentas on-line oferecidas pelo GitHub para simular alguém fazendo alterações no remote enquanto trabalhamos.
Vá para o seu fork
deste repositório no github.com e abra o arquivo Alice.txt
.
Encontre o botão de editar o arquivo, faça uma alteração e crie o commit através do site.
Neste repositório adicionei uma alteração remota ao Alice.txt
em uma branch chamada fetching_changes_sample
, mas na sua versão do repositório você pode, é claro, alterar o arquivo na master
.
Buscando as alterações (Fetch)
Ainda lembramos que quando você executa git push
, sincroniza as alterações feitas no Local Repository no Remote Repository.
Para obter as alterações feitas no Remote no seu Local Repository, você usa o git fetch
.
Isso obtém qualquer alteração do remoto - commits e branches - no seu Local Repository.
Observe que, neste ponto, as alterações ainda não estão integradas nas branches locais e, portanto, no Working Directory e na Staging Area.
Se você executar git status
agora, verá outro ótimo exemplo de comandos git dizendo exatamente o que está acontecendo:
> git status
On branch fetching_changes_sample
Your branch is behind 'origin/fetching_changes_sample' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Puxando as alterações (Pull)
Como não temos nenhuma alteração em working ou staged, podemos executar o git pull
agora para obter as alterações do Remote Repository até a nossa área de trabalho.
Puxar implicitamente também faz
fetch
do Remote Repository, mas as vezes é uma boa idéia fazer umfetch
por si só. Por exemplo, quando você deseja sincronizar qualquer nova branch remote, ou quando deseja garantir que seu Local Repository esteja atualizado antes de fazer umagit rebase
em algo comoorigin/master
.
Antes de puxarmos (pull
), vamos alterar um arquivo localmente para ver o que acontece.
Vamos alterar o arquivo Alice.txt
no nosso Working Directory!
Agora, se você tentar fazer um git pull
, verá o seguinte erro:
> git pull
Updating df3ad1d..418e6f0
error: Your local changes to the following files would be overwritten by merge:
Alice.txt
Please commit your changes or stash them before you merge.
Aborting
Você não pode executar pull
enquanto existirem modificações nos arquivos no Working Directory que também são alteradas pelos commits que você está puxando (pull
).
Embora uma maneira de contornar isso seja adicioná-las (add
) ao Staging Environment e criar o commit
, este é um bom momento para aprender sobre outra excelente ferramenta, o git stash
.
Escondendo as alterações (Stash)
Se a qualquer momento você tiver alterações locais que ainda não deseja colocar em um commit, ou deseja armazenar em algum lugar enquanto tenta alguma forma diferente de resolver um problema, você pode esconder essas alterações.
Um git stash
é basicamente uma pilha de alterações nas quais você armazena as alterações no Working Directory.
Os comandos que você mais irá utilizar são o git stash
, que coloca qualquer modificação feita no Working Directory em stash (ocultada), e o git stash pop
, que recebe a última alteração que foi salva em stash e a aplica ao Working Directory novamente.
Assim como os comandos de pilha com o nome, o git stash pop
remove a última alteração escondida antes de aplicá-la novamente.
Se você não deseja remover as alterações do stash no momento de aplicá-las, pode usar o git stash apply
.
Para inspecionar o seu stash
atual, você pode usar git stash list
para listar as entradas individuais, e git stash show
para mostrar as alterações da última entrada no stash
.
Outro comando interessante é o
git stash branch {BRANCH NAME}
, que cria um branch a partir do HEAD no momento em que você armazenou as alterações e aplica as alterações armazenadas nessa branch.
Agora que sabemos sobre git stash
, vamos executá-lo para remover nossas alterações locais em Alice.txt
do Working Directory para que possamos prosseguir e puxar (git pull
) as alterações que fizemos no Github.
Depois disso, vamos executar git stash pop
para recuperar as alterações.
Como tanto o commit que puxamos quanto a alteração que ocultamos (stash
) modificam Alice.txt
, você terá que resolver o conflito da mesma forma que faria em um merge
ou rebase
. Quando terminar, adicione (add
) e commite a alteração.
Puxando (Pull) com conflitos
Agora que entendemos como buscar (fetch
) e puxar (pull
) as mudanças remotas em nosso Dev Environment, é hora de criar alguns conflitos!
Não mande push
do commit que mudou Alice.txt
e volte ao seu Remote Repository em github.com.
Lá vamos mudar Alice.txt
novamente e commitar a alteração.
Agora existem dois conflitos entre nossos Local e Remote Repositories.
Não se esqueça de executar o git fetch
para ver a mudança remota sem puxá-la (pull
) imediatamente.
Se você executar o git status
, verá que as duas branches têm um commit nelas diferente da outra.
> git status
On branch fetching_changes_sample
Your branch and 'origin/fetching_changes_sample' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
Além disso, alteramos o mesmo arquivo em ambos os commits para introduzir um conflito de merge
que precisaremos resolver.
Quando você executa git pull
enquanto existe uma diferença entre o Local e o Remote Repository, exatamente a mesma coisa acontece quando você executa merge
de 2 branches.
Além disso, você pode pensar no relacionamento entre ramificações no Remote e aquele no Local Repository como um caso especial de criação de uma ramificação com base em outra.
Uma ramificação local é baseada no estado de uma ramificação no Remote desde a última vez que você a buscou (fetch
).
Pensando assim, as duas opções que você possui para obter mudanças remotas fazem muito sentido:
Quando você executa git pull
, as versões Local e Remote de um ramo serão mescladas (merge
). Assim como mesclagem de branches isso apresentará um commit de merge.
Como qualquer ramificação local é baseada em sua respectiva versão remote, também podemos executar rebase
, para que qualquer alteração que possamos ter feito localmente apareçam como se fossem baseadas na versão mais recente disponível no Remote Repository.
Para fazer isso, podemos usar git pull --rebase
(ou a abreviação git pull -r
).
Conforme detalhado na seção Rebasing, há um benefício em manter um histórico linear limpo, e é por isso que eu recomendo fortemente que sempre que você for executar git pull
, execute como git pull -r
.
Você também pode dizer ao git para usar
rebase
em vez demerge
como estratégia padrão quando executargit pull
, configurando a configuraçãopull.rebase
com um comando como este:git config --global pull.rebase true
.
Se você ainda não executou o git pull
quando o mencionei há alguns parágrafos atrás, agora vamos executar o git pull -r
para obter as mudanças remotas, fazendo parecer que nosso novo commit aconteceu depois delas.
Obviamente, como em uma rebase
normal (ou merge
) você terá que resolver o conflito que introduzimos para que o git pull
finalize.
Cherry-picking
Parabéns! Você chegou aos recursos mais avançados!
Agora você entende como usar todos os comandos git típicos e, mais importante, como eles funcionam.
Esperamos que isso torne os conceitos a seguir muito mais simples de entender do que se eu tivesse acabado de dizer quais comandos digitar.
Então, vamos direto ao assunto e aprender fazer
cherry-pick
de commits!
Curiosidade: A tradução de
cherry-pick
é colher cereja.
Você ainda se lembra mais ou menos do que um commit
é feito, certo?
E como seus commits são aplicados como novos commits, com o mesmo change set e message quando você faz o rebase
de uma branch?
Sempre que você quiser apenas fazer algumas alterações de uma branch e aplicá-las a outra branch, você precisa fazer cherry-pick
desses commits e colocá-los em sua branch.
É exatamente isso que o git cherry-pick
permite que você faça com commits isolados ou com um agrupado de commits.
Assim como durante um rebase
, isso realmente colocará as alterações desses commits em um novo commit em sua branch atual.
Vamos dar uma olhada nos exemplos de cada cherry-pick
com um ou mais commits.
A figura abaixo mostra três branches antes de fazermos qualquer coisa. Vamos supor que realmente queremos obter algumas mudanças da branch add_patrick
na branch change_alice
. Infelizmente, eles ainda não entraram no master, portanto, não podemos apenas executar rebase
na master para obter essas alterações (juntamente com outras alterações na outra branch, que talvez nem desejemos).
Então vamos fazer git cherry-pick
do commit 63fc421.
A figura abaixo mostra o que acontece quando executamos git cherry-pick 63fc421
Como você pode ver, um novo commit com as alterações que queríamos aparece na branch.
Neste ponto observe que, como qualquer outro tipo de alteração de uma branch que já vimos antes, qualquer conflito que ocorra durante um
cherry-pick
precisará ser resolvido por nós, antes que o comando possa ser executado.Como todos os outros comandos, você pode continuar (
--continue
) umcherry-pick
quando resolver os conflitos, ou abortar (--abort
) o comando por completo.
A figura abaixo mostra um cherry-pick
de um conjunto de commits em vez de um único. Você pode simplesmente fazer isso chamando o comando no formato git cherry-pick <from>..<to>
ou, no nosso exemplo abaixo, como git cherry-pick 0cfc1d2..41fbfa7
.
Reescrevendo a história
Estou me repetindo agora, mas você ainda se lembra de [
rebase
](# rebasing) bem o suficiente, certo? Caso contrário, volte rapidamente para essa seção antes de continuar aqui, pois usaremos o que já sabemos enquanto aprendemos a mudar o histórico!
Como você sabe, um commit
contém basicamente suas alterações, uma mensagem e algumas outras coisas.
A história de uma branch é composta por todos os seus commits.
Mas digamos que você acabou de fazer um commit
e, em seguida, observou que esqueceu de adicionar um arquivo ou que você cometeu um erro de digitação e a alteração deixou você com um código com bug.
Examinaremos brevemente duas coisas que poderíamos fazer para corrigir isso e fazer parecer que nunca aconteceu.
Vamos mudar para uma nova branch com git checkout -b rewrite_history
.
Agora faça algumas alterações em Alice.txt
e Bob.txt
e, em seguida, execute git add Alice.txt
.
Então faça o commit
usando uma mensagem como "Essa é uma história" e pronto.
Espere, eu disse que terminamos? Não, você verá claramente que cometemos alguns erros aqui:
- Nós esquecemos de adicionar as mudanças de
Bob.txt
- Nós não escrevemos uma boa mensagem de commit
Alterando o último commit (Amend)
Uma maneira de corrigir ambos os itens de uma só vez seria alterar (amend
) o commit que acabamos de fazer.
Alterar o último commit basicamente funciona como criar um novo.
Antes de fazer qualquer coisa, dê uma olhada no seu último commit, com git show {COMMIT}
. Coloque o hash de confirmação (que você provavelmente ainda verá em sua linha de comando ao ter executado git commit
ou no git log
), ou apenas HEAD.
Assim como no git log
, você verá a mensagem, o autor, a data e, claro, as alterações.
Agora vamos alterar (amend
) o que fizemos nesse commit.
Execute git add Bob.txt
para enviar as alterações para a Staging Area e, em seguida, git commit --amend
.
O que acontece a seguir é o desenrolar do commit, as novas alterações da Staging Area adicionadas no commit existente e a abertura do editor da mensagem de commit.
No editor, você verá a mensagem de commit anterior. Sinta-se livre para alterá-lo para algo melhor.
Depois que você terminar, dê uma olhada no último commit com git show HEAD
.
Como você certamente já esperava, o hash de confirmação é diferente. O commit original se foi e, em seu lugar, existe um novo, com as alterações combinadas e a nova mensagem de commit.
Observe como os outros dados de commit, como autor e data, não são alterados em relação ao commit original. Você pode mexer com eles também, se você realmente quiser, usando os sinalizadores extras
--author={AUTHOR}
e--date={DATE}
ao alterar.
Parabéns! Você acabou de reescrever a história pela primeira vez!
Rebase interativo
Geralmente, quando executamos git rebase
, nós fazemos rebase
em uma branch. Quando fazemos algo como git rebase origin/master
, o que realmente acontece é um rebase no HEAD dessa branch.
De fato, se quiséssemos, poderíamos fazer rebase
em qualquer commit.
Lembre-se de que um commit contém informações sobre o histórico que veio antes dele
Como muitos outros comandos, o git rebase
possui um modo interactive.
Diferente da maioria dos outros, o rebase
interativo é algo que você provavelmente estará usando muito, pois permite alterar o histórico o quanto quiser.
Especialmente se você seguir um fluxo de trabalho fazendo muitos pequenos commits de suas alterações, o que lhe permitirá voltar facilmente se cometer um erro,rebase
interativo será o seu aliado mais próximo.
Chega de conversa! Vamos fazer algo!
Volte para a sua branch master e faça git checkout
de uma nova branch para trabalharmos nela.
Como antes, faremos algumas alterações em Alice.txt
e Bob.txt
e, em seguida, executaremos git add Alice.txt
.
Em seguida, faça git commit
usando uma mensagem como "Adicionar texto em Alice ".
Agora, em vez de alterar esse commit, execute git add Bob.txt
e git commit
. Como mensagem, usei "Adicionar Bob.txt".
E para tornar as coisas mais interessantes, faremos outra alteração em Alice.txt
, na qual faremos git add
e git commit
. Como mensagem, usei "Adicionar mais texto a Alice".
Se agora analisarmos o histórico da branch com git log
(ou apenas uma rápida olhada, de preferência com git log --oneline
), veremos nossos três commits em cima do que estiver na sua master.
Para mim, aparece assim:
> git log --oneline
0b22064 (HEAD -> interactiveRebase) Adicionar mais texto a Alice
062ef13 Adicionar Bob.txt
9e06fca Adicionar texto em Alice
df3ad1d (origin/master, origin/HEAD, master) Adicionar Alice
800a947 Adicionar texto do tutorial
Há duas coisas que gostaríamos de corrigir sobre isso, que, com o objetivo de aprender coisas diferentes, serão um pouco diferentes do que na seção anterior sobre amend
:
- Coloque as duas alterações de
Alice.txt
em um único commit - Nomeie as coisas de forma consistente e remova o .txt da mensagem sobre
Bob.txt
Para alterar os três novos commits, queremos fazer um rebase no commit antes deles. Esse commit para mim é df3ad1d
, mas também podemos referenciá-lo como o terceiro commit do atual HEAD como HEAD~3
Para iniciar um rebase
interativo, usamos git rebase -i {COMMIT}
, então vamos executar git rebase -i HEAD~3
O que você verá é o editor de sua escolha mostrando algo como isto:
pick 9e06fca Adicionar texto em Alice
pick 062ef13 Adicionar Bob.txt
pick 0b22064 Adicionar mais texto a Alice
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
Observe que o git
sempre explica tudo o que você pode fazer quando você chama o comando.
Os comandos (Commands) que você provavelmente mais usará são reword
, squash
e drop
. (E pick
, mas esse está lá por padrão)
Reserve um momento para pensar sobre o que você vê e o que vamos usar para alcançar nossos dois objetivos de cima. Eu vou esperar.
Tem um plano? Perfeito!
Antes de começarmos a fazer alterações, observe que os commits são listados do mais antigo para o mais novo e, portanto, na direção oposta à saída do git log
.
Vou começar com a alteração fácil e fazer com que possamos alterar a mensagem do commit do meio.
pick 9e06fca Adicionar texto em Alice
reword 062ef13 Adicionar Bob.txt
pick 0b22064 Adicionar mais texto a Alice
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
[...]
Agora para obter as duas alterações do Alice.txt
em um commit.
Obviamente, o que queremos fazer é squash
(compactar) o último dos dois commits no primeiro, então vamos colocar esse comando no lugar do pick
no segundo commit, alterando Alice.txt
. Para mim, no exemplo, isso é 0b22064.
pick 9e06fca Adicionar texto em Alice
reword 062ef13 Adicionar Bob.txt
squash 0b22064 Adicionar mais texto a Alice
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
[...]
Nós terminamos? Isso fará o que queremos?
Não vai, né? Como os comentários no arquivo nos dizem:
# s, squash = use commit, but meld into previous commit
Portanto, o que fizemos até agora mesclará (merge) as alterações do segundo commit de Alice, com o commit de Bob. Não é isso que queremos.
Outra coisa poderosa que podemos fazer em um rebase
interativo é mudar a ordem dos commits.
Se você leu com atenção o que os comentários disseram, você já sabe como: Simplesmente mova as linhas!
Felizmente, você está no seu editor de texto favorito, então vá em frente e mova o segundo commit Alice para ficar logo após o primeiro.
pick 9e06fca Adicionar texto em Alice
squash 0b22064 Adicionar mais texto a Alice
reword 062ef13 Adicionar Bob.txt
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
[...]
Isso deve funcionar, então feche o editor e diga ao git
para começar a executar os comandos.
O que acontece a seguir é como uma rebase
normal: começando com o commit que você referenciou no início, cada um dos commits que você listou será aplicado um após o outro.
Neste momento isso não acontecerá, mas quando você reordenar as alterações do código, poderá ocorrer que você entre em conflito durante o
rebase
. Afinal, você possivelmente misturou as mudanças que estavam desenvolvendo.Apenas resolva eles como faria normalmente.
Após aplicar o primeiro commit, o editor abrirá e permitirá que você coloque uma nova mensagem para o commit combinando as alterações em Alice.txt
. Joguei fora o texto dos dois commits e coloquei "Adicionar vários textos importantes em Alice".
Depois de fechar o editor para concluir o commit, ele será aberto novamente para permitir que você altere a mensagem do commit Adicionar Bob.txt
. Remova o ".txt" e continue fechando o editor.
É isso aí! Você reescreveu a história novamente. Desta vez, muito mais substancialmente do que quando utilizamos amend
!
Se você olhar o git log
novamente verá que há dois novos commits no lugar dos três que tínhamos anteriormente. Mas agora você já está acostumado com o que o rebase
faz com commits e estava esperando por isso.
> git log --oneline
105177b (HEAD -> interactiveRebase) Adicionar Bob
ed78fa1 Adicionar vários textos importantes em Alice
df3ad1d (origin/master, origin/HEAD, master) Adicionar Alice
800a947 Adicionar texto do tutorial
História pública, por que você não deve reescrevê-la e como fazer isso com segurança
Como observado anteriormente, a alteração do histórico é uma parte incrivelmente útil de qualquer fluxo de trabalho que envolve fazer muitos pequenos commit enquanto você trabalha.
Embora todas as pequenas alterações atômicas tornem muito fácil para você, por exemplo, verificar se a cada alteração que seu conjunto de testes ainda passa e, se não, remover ou emendar apenas essas alterações específicas, os 100 commits que você fez para escrever HelloWorld.java
provavelmente não são algo que você deseja compartilhar com as pessoas .
Muito provavelmente o que você deseja compartilhar com eles são algumas alterações bem-formadas, com boas mensagens de commit, informando aos colegas o que você fez por qual motivo.
Enquanto todos esses pequenos commits existirem apenas no seu Dev Environment, você estará perfeitamente seguro para fazer um git rebase -i
e alterar o histórico para o conteúdo do seu coração.
As coisas ficam problemáticas quando se trata de mudar a história pública. Isso significa qualquer coisa que já tenha chegado ao Remote Repository.
Nesse ponto, tornou-se público e as branches de outras pessoas podem se basear nessa história. Isso realmente faz com que você geralmente não queira mexer.
O conselho usual é "nunca reescrever a história pública!" e enquanto repito isso aqui, devo admitir que há uma quantidade decente de casos em que você ainda pode reescrever a história pública.
Em todos esses casos, a história não é "realmente" pública. Você certamente não deseja reescrever o histórico na branch master de um projeto de código aberto, ou algo como a branch release da sua empresa.
Onde você pode querer reescrever a história são branches que você empurrou (push
) apenas para compartilhar com alguns colegas.
Você pode estar desenvolvendo em trunk-based, mas deseja compartilhar algo que ainda não é compilado, portanto, obviamente, não deseja colocar isso na branch principal conscientemente. Ou você pode ter um fluxo de trabalho no qual compartilha branch de feature.
Especialmente com as branches de feature, espero que você faça rebase
com freqüência no master atual. Mas, como sabemos, um git rebase
adiciona os commits de nossas branches à medida que novos commits se baseiam naquilo em que os baseamos. Isso reescreve a história. E, no caso de uma branch de feature compartilhada, ele reescreve a história pública.
Então, o que devemos fazer se seguirmos o mantra "Nunca reescrever a história pública"?
Nunca fazer rebase da nossa branch e esperar que ele ainda mergeie com a master no final?
Não usar branches de features compartilhadas?
É certo que o segundo é realmente uma resposta razoável, mas você ainda não pode fazer isso. Portanto, a única coisa que você pode fazer é aceitar reescrever a história pública e empurrar (push
) o histórico alterado para o Remote Repository.
Se você fizer um git push
, você será notificado de que não está autorizado a fazer isso, pois sua branch local divergiu da remote.
Você precisará forçar (force
) o envio das alterações e substituir o remoto pela sua versão local.
Como destaquei isso de forma sugestiva, você provavelmente está pronto para tentar o git push --force
no momento. Você realmente não deveria fazer isso se quiser reescrever a história pública em segurança!
Você está muito melhor usando o irmão mais cuidadoso do --force
, --force-with-lease
!
O --force-with-lease
irá verificar se a sua versão local da branch remote e o atual remote correspondem, antes de fazer o push
.
Com isso, você pode garantir que não irá apagar acidentalmente nenhuma alteração que alguém possa ter dado push
enquanto você reescreveu o histórico!
E nessa nota, deixarei você com um mantra ligeiramente alterado:
Não reescreva o histórico público, a menos que tenha certeza do que está fazendo. E se você o fizer, esteja seguro e force-with-lease.
Lendo a história
Conhecendo as diferenças entre as áreas em seu Dev Environment - especialmente o Local Repository - e como os commits e o histórico funcionam, fazer um rebase
não deve ser assustador para você.
Mesmo assim, as vezes as coisas dão errado. Você pode ter feito um rebase
e acidentalmente aceitado a versão errada do arquivo ao resolver um conflito.
Agora, em vez do recurso que você adicionou, apenas os seus colegas adicionaram a linha de logon em um arquivo.
Felizmente o git
está ao seu lado por ter um recurso de segurança interno chamado logs de referência (Reference Logs), também conhecido como reflog
.
Sempre que qualquer referência como a ponta de uma branch é atualizada no seu Local Repository, uma entrada no Log de Referência é adicionada.
Portanto, há um registro de qualquer momento em que você faz um commit
, mas também de quando você redefiniu (reset
) ou moveu o HEAD
etc.
Depois de ler este tutorial até agora, você vê como isso pode ser útil quando estragamos um rebase
, certo?
Sabemos que um rebase
move o HEAD
da nossa branch até o ponto em que o baseamos e aplica nossas alterações. Um rebase
interativo funciona da mesma forma, mas pode fazer coisas com esses commits como squashing ou rewording eles.
Se você ainda não está na branch em que praticamos o rebase interativo, mude para ela novamente, pois estamos prestes a praticar um pouco mais lá.
Vamos dar uma olhada no reflog
das coisas que fizemos nessa branch - você adivinhou como - executando o git reflog
.
Você provavelmente verá muita informação na saída, mas as primeiras linhas na parte superior devem ser semelhantes a esta:
> git reflog
105177b (HEAD -> interactiveRebase) HEAD@{0}: rebase -i (finish): returning to refs/heads/interactiveRebase
105177b (HEAD -> interactiveRebase) HEAD@{1}: rebase -i (reword): Adicionar Bob
ed78fa1 HEAD@{2}: rebase -i (squash): Adicionar vários textos importantes em Alice
9e06fca HEAD@{3}: rebase -i (start): checkout HEAD~3
0b22064 HEAD@{4}: commit: Adicionar mais texto a Alice
062ef13 HEAD@{5}: commit: Adicionar Bob.txt
9e06fca HEAD@{6}: commit: Adicionar texto em Alice
df3ad1d (origin/master, origin/HEAD, master) HEAD@{7}: checkout: moving from master to interactiveRebase
Aí está. Tudo o que fizemos, desde a mudança para a branch até o rebase
.
É muito legal ver as coisas que fizemos, mas inútil por si só se erramos em algum lugar, se não fosse pelas referências no início de cada linha.
Se você comparar a saída de reflog
com a última vez que examinamos o log
, verá esses pontos relacionados às referências de commit, e podemos usá-las dessa maneira.
Digamos que realmente não quiséssemos fazer o rebase. Como nos livramos das alterações feitas?
Nós movemos o HEAD
para o ponto anterior ao rebase
iniciado com um git reset 0b22064
.
0b22064
é o commit antes derebase
no meu caso. De um modo mais geral, você também pode fazer referência a ele como HEAD de quatro mudanças atrás viaHEAD@{4}
. Observe que, se você tiver alternado entre as branches ou tiver feito alguma outra coisa que crie uma entrada de log, poderá ter um número maior lá.
Se você der uma olhada no log
agora, verá o estado original com três commits individuais restaurados.
Mas digamos que agora percebemos que não era isso que queríamos. O rebase
está bom, nós simplesmente não gostamos de como mudamos a mensagem do commit de Bob.
Nós poderíamos simplesmente fazer outro rebase -i
no estado atual, exatamente como fizemos originalmente.
Ou usamos o reflog e voltamos para depois do rebase e alteramos o commit a partir daí com amend
.
Mas agora você já sabe como fazer isso, então deixarei você tentar por conta própria. Além disso, você também sabe que existe o reflog
que permite desfazer a maioria das coisas que você pode acabar fazendo por engano.
Aprendeu algo com o treinamento? É quase de graça, basta deixar uma star ⭐ no repositório.