Introdução ao Git

Publicado em 16/03/2015

Antigamente, quando a gente escrevia usando lápis e papel, não era difícil vermo-nos obrigados a revirar o lixo, atrás de alguma coisa descartada por engano, muitas vezes sem sucesso…​ e lá se iam horas ou até mesmo dias de trabalho, irremediavelmente perdidos.

Sistemas de Controle de Versão

Com a advento da editoração eletrônica, esse tipo de problema cresceu sobremaneira. A facilidade de apagar um arquivo no computador, seja intencionalmente ou não, é enorme. Algumas vezes, como no caso dos papéis jogados no lixo, é possível recuperá-los, mas na maioria delas temos que recomeçar do zero.

Quando estamos criando alguma coisa, escrevendo um livro ou um programa de computador, por exemplo, vamos constantemente modificando o texto ou o código e, não raro, lamentamos ter sobrescrito algum trecho que gostaríamos de trazer de volta.

Quem já não passou por esse tipo de situação?

Pois é exatamente para tentar resolver, ou ao menos amenizar, esse problema, que surgiram os sistemas de controle de versão (VCS, do inglês Version Control System), também conhecidos como gerenciamento de código fonte (SCM, do inglês Source Code Management), ou simplesmente versionamento, em português.

Trataremos neste artigo do controle de versão para desenvolvimento de software, embora ele possa ser usado para quase todo o tipo de arquivo que existe num computador.

Na verdade, os VCS foram desenvolvidos exatamente para controlar o desenvolvimento de software, permitindo não apenas a reversão de um projeto para um estado anterior, mas também para comparar modificações ao longo do tempo, descobrir quem introduziu determinada mudança que esteja causando problemas, quem apresentou uma sugestão ou reportou um bug, e muito mais.

O Diferencial do Git

A Wikipédia lista entre as soluções de código aberto mais comuns: CVS, Mercurial, Git e SVN; e entre as comerciais, destaca: SourceSafe, TFS, PVCS (Serena) e ClearCase.

A primeira pergunta que surge, então, é: o que torna o Git diferente dos outros VCS e qual a razão da sua crescente popularidade?

Certamente a diferença mais óbvia é que o Git é um sistema distribuído (ao contrário do SVN ou TFS, por exemplo). Isso significa que você possui um repositório local que fica numa pasta especial chamada .git e normalmente (mas não obrigatoriamente), tem um repositório remoto central onde diferentes colaboradores podem contribuir para o desenvolvimento do código. Observe que cada um desses colaboradores possui um clone exato do repositório em suas estações de trabalho locais.

Na documentação do Git há uma figura, reproduzida abaixo, que ilustra perfeitamente o controle de versão baseado num sistema distribuído.

Cont. de Versão Distribuído

Como podemos ver, nos sistemas distribuídos, os clientes não apenas verificam o último snapshot (instantâneo) dos arquivos, mas espelham o repositório inteiro. Uma enorme vantagem dessa solução é que, se porventura o servidor perder os dados por algum problema técnico, eles podem ser recuperados de qualquer um dos clientes que estejam colaborando no projeto, pois cada clone possui um backup completo dos dados.

Quem quiser se aprofundar mais no funcionamento cliente/servidor dos sistemas de controle de versão, poderá tomar a referência mencionada da Wikipédia como ponto de partida.

A seguir, numa adaptação do excelente artigo de Juri Strumpflohner, publicado no seu blog em abril de 2013, vamos analisar o funcionamento do Git observando o repositório Git sob o ponto de vista das árvores que ele constrói. Para isso, utilizaremos algumas funcionalidades comuns, tais como:

  • acrescentar/modificar um arquivo novo;
  • criar e mesclar uma branch (ramificação) com e sem conflitos de mesclagem;
  • ver o histórico/changelog;
  • executar um rollback até determinado commit e
  • compartilhar/sincronizar o código num repositório remoto central.

A próxima figura, editada a partir da original contida na documentação oficial do Git, ilustra com clareza as três fases principais por que passa um arquivo num diretório controlado pelo Git:

Diretório de trabalho, área de estágio e diretório Git

Terminologia Básica

Vamos começar examinando alguns termos usados no Git que são fundamentais:

  • master — a branch principal do repositório. Normalmente é onde as pessoas trabalham e na qual a integração acontece.
  • clone — copia um repositório git existente, usualmente de alguma localização remota, para o seu ambiente local.
  • commit — envio de arquivos para o repositório local.
  • fetch ou pull — pega as últimas atualizações de um repositório remoto. A diferença entre fetch e pull é que pull combina as duas coisas: pega as atualizações de código do repositório remoto e efetua sua mesclagem com o repositório local.
  • push — é usado para enviar arquivos para um repositório remoto.
  • remote — são os locais remotos do seu repositório, normalmente em algum servidor central, como o GitHub.
  • SHA — cada commit ou node na árvore Git é identificado por uma chave SHA única. Pode-se utilizá-las em vários comandos para manipular um node específico.
  • head — é uma referência a um node para o qual nosso espaço de trabalho no repositório aponta no momento.
  • branch — uma branch no Git nada mais é do que um rótulo particular em determinado node.

Configuração Elementar

Não vou entrar em detalhes aqui sobre a instalação do Git, pois isso vai depender do sistema operacional de cada um e, além disso, já existem instruções detalhadas para isso na documentação oficial do Git, com tradução em português.

Usarei a sintaxe da linha de comando dos sistemas baseados em Unix, tais como o Linux e o OS X. No Windows, além da interface gráfica, há um programa para emular um terminal tipo Unix, de forma que este tutorial poderá ser seguido sem problemas, seja qual for seu sistema operacional. O símbolo $ representa o prompt da linha de comando e não deve ser digitado.

Pressupondo, então, que o Git já está instalado no seu computador e o atalho correspondente ao diretório de sua instalação foi devidamente incluído na variável PATH do seu sistema, a primeira coisa a ser feita é configurá-lo com seu nome e endereço de email. Vamos aproveitar também para configurar um editor-padrão — vou escolher o vim, mas esteja à vontade para personalizar esta seleção de acordo com o seu gosto pessoal. Para isso, entre os seguintes comandos, com as devidas substituições pessoais:

$ git config --global user.name "J A Gaeta Mendes" $ git config --global user.email "meu_email@example.com" $ git config --global core.editor vim

As configurações podem ser conferidas com o comando git config --list, cujo resultado, após os comandos acima, será:

$ git config --list user.name=J A Gaeta Mendes user.email=meu_email@example.com core.editor=vim

Criação de um novo repositório Git

Antes de prosseguir, vamos criar um diretório onde pretendemos instalar um novo repositório Git e entrar neste diretório, com os comandos:

$ mkdir meu_repo_git $ cd meu_repo_git

Em seguida, vamos inicializar nosso novo repositório git:

$ git init Initialized empty Git repository in /home/gaeta/meu_repo_git/.git/

Use o comando git status para verificar o estado atual do repositório:

$ git status On branch master Initial commit nothing to commit (create/copy files and use "git add" to track)

Criação e commit de um novo arquivo

O próximo passo é criar e adicionar algum conteúdo ao repositório. Vamos criar um arquivo de uma forma bem simples, com os comandos:

$ touch ola.txt $ echo Ola, mundo! > ola.txt

Uma nova verificação do estado do repositório vai apresentar agora a seguinte situação:

$ git status On branch master Initial commit Untracked files: (use "git add <file>..." to include in what will be committed) ola.txt nothing added to commit but untracked files present (use "git add" to track)

Antes de fazer um commit, precisamos registrar o arquivo, o que fazemos com o comando add:

$ git add ola.txt

Mais uma verificação do estado:

$ git status On branch master Initial commit Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: ola.txt

Podemos agora, finalmente, fazer seu commit para o repositório:

$ git commit -m "Acrescenta meu primeiro arquivo" [master (root-commit) 3a40877] Acrescenta meu primeiro arquivo 1 file changed, 1 insertion(+) create mode 100644 ola.txt

Se examinarmos neste instante a árvore do repositório, teríamos a seguinte situação:

Árvore do repositório 1

Há um node para o qual o "rótulo" master aponta.

Acrescentando outro arquivo

Vamos agora adicionar um outro arquivo ao repositório:

$ echo "Oi, sou um outro arquivo" > outro_arquivo.txt $ git add . $ git commit -m "acrescenta outro arquivo com outro conteúdo" [master ad5c381] acrescenta outro arquivo com outro conteudo 1 file changed, 1 insertion(+) create mode 100644 outro_arquivo.txt

Observe que agora usamos git add ., que acrescenta todos os arquivos do diretório corrente (.). Do ponto de vista da árvore temos agora outro node e o master moveu-se para ele.

Árvore do repositório 2

Criando uma ramificação (de recursos)

Ramificação (branching) e mesclagem (merging) são as duas coisas que tornam o Git tão poderoso, e é para isso que ele foi otimizado, em se tratando de um sistema de controle de versão distribuído (VCS). De fato, as ramificações de recursos (feature branches) são criadas para cada novo tipo de funcionalidade que você acrescentar ao seu sistema e são normalmente apagadas mais tarde, depois que o recurso tenha sido mesclado outra vez na ramificação principal de integração (usualmente a ramificação master). A grande vantagem disso é que você pode experimentar a nova funcionalidade num “playground” separado e isolado, movendo-se rapidamente para frente ou para trás da ramificação “master” original quando necessário. Ademais, ela poderá ser facilmente descartada outra vez (no caso de não ser mais necessária), bastando eliminar a ramificação do recurso. Há um ótimo artigo para entender as ramificações no Git, que você definitivamente deve ler.

Mas vamos começar. Primeiro de tudo, criamos uma nova ramificação de recurso:

$ git branch minha-feature-branch

Em seguida, podemos usar o comando git branch para obter uma lista das ramificações atuais:

$ git branch * master minha-feature-branch

O * na frente de master indica que no momento estamos naquele ramo. Vamos agora mudar para o ramo minha-feature-branch.

$ git checkout minha-feature-branch Switched to branch 'minha-feature-branch'

Conferindo:

$ git branch master * minha-feature-branch

A diferença de outros VCS é que há apenas um diretório de trabalho. Todas as ramificações ficam nele e não há uma pasta separada para cada ramificação criada. Ao invés disso, quando se alterna entre as ramificações, o Git atualiza o conteúdo do diretório de trabalho para refletir aquele da ramificação para a qual se está mudando.

Vamos modificar um dos nossos arquivos existentes:

$ echo "Oi" >> ola.txt $ cat ola.txt Ola, mundo! Oi

…​e então vamos fazer um commit dele para nossa nova ramificação:

$ git commit -a -m "modifica arquivo acrescentando oi" [minha-feature-branch 8cff170] modifica arquivo acrescentando oi 1 file changed, 1 insertion(+)

Vejam, na figura a seguir, como ficou a árvore do git depois de todos esses comandos:

Árvore do repositório 3

Até aqui tudo parece bem normal e ainda temos uma linha reta na árvore, mas observe que agora o master continuou onde estava e movemos adiante minha-feature-branch.

Vamos trocar para master e modificar o mesmo arquivo lá também.

$ git checkout master Switched to branch 'master'

Como era de se esperar, alo.txt não foi modificado:

$ cat ola.txt Ola, mundo!

Vamos modificá-lo e dar um commit no master também (isso vai gerar um belo conflito mais tarde):

$ echo "Oi, fui modificado no master" >> ola.txt $ git commit -a -m "acrescenta linha em ola.txt" [master 33b65ea] acrescenta linha em ola.txt 1 file changed, 1 insertion(+)

Nossa árvore agora visualiza a ramificação:

Árvore do repositório 4

Mesclando e resolvendo conflitos

O próximo passo será reintegrar nossa feature branch de volta em master. Isso é feito usando o comando merge :

$ git merge minha-feature-branch Auto-merging ola.txt CONFLICT (content): Merge conflict in ola.txt Automatic merge failed; fix conflicts and then commit the result.

Como esperado, temos um conflito de mesclagem em ola.txt.

$ cat ola.txt Ola, mundo! <<<<<<< HEAD Oi, fui modificado no master ======= Oi >>>>>>> minha-feature-branch

Vamos consertar isso num editor de textos:

Ola, mundo! Oi, fui modificado no master Oi

…​e fazer o commit novamente:

$ git commit -a -m "resolve conflitos de merge" [master 838d26a] resolve conflitos de merge

A árvore reflete nosso merge:

Árvore do repositório 5

Indo para um determinado Commit

Suponhamos que queremos voltar a um dado commit. Podemos usar o comando git log para obter todos os identificadores SHA que identificam de forma única cada node da árvore.

Escolha um dos identificadores (mesmo que não seja o número completo, pouco importa) e vá para aquele node usando o comando checkout:

$ git checkout 33b65ea Note: checking out '33b65ea'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b new_branch_name HEAD is now at 33b65ea... acrescenta linha em ola.txt

Observe o comentário exibido pelo git. O que ele significa? Detached head significa que a head não está mais apontando para um rótulo de branch, mas, ao invés disso, para um commit específico da árvore.

Depois de modificar o arquivo ola.txt e fazer commit da mudança, a árvore ficará assim:

Árvore do repositório 6

Como você pode ver, o node recem-criado não possui nenhum rótulo. A única referência que atualmente aponta para ele é a head. Todavia, se trocarmos para master novamente, o commit anterior será perdido, pois não teremos como voltar para aquele node.

$ git checkout master Warning: you are leaving 1 commit behind, not connected to any of your branches: 576bcb8 change file undoing previous changes If you want to keep them by creating a new branch, this may be a good time to do so with: git branch new_branch_name 576bcb8239e0ef49d3a6d5a227ff2d1eb73eee55 Switched to branch 'master'

Como se observa, o git nos alerta desse fato.

Rollback

Voltar para trás é bom, mas e se quisermos desfazer tudo, voltando ao estado antes do merge da feature branch? Muito fácil:

$ git reset --hard 33b65ea HEAD is now at 33b65ea acrescenta linha em ola.txt

A sintaxe genérica aqui é git reset --hard <tag/branch/commit id>.

Usando “revert” para fazer rollback das mudanças do jeito fácil

Se você precisar fazer o rollback de um commit inteiro e (o que é pior) você já sincronizou com um repositório remoto, então usar git reset --hard pode não ser adequado, uma vez que dessa forma você estaria reescrevendo a história, o que não é permitido se a sincronização com o servidor remoto já foi efetuada.

Em tais situações pode-se usar o comando revert, o qual basicamente cria um novo commit desfazendo todas as mudanças de um determinado commit que for especificado. Considere, por exemplo, que você queira fazer o rollback de um commit com o ID 41b8684:

$ git revert 41b8684

Desfazendo mudanças quando não foi feito Commit

Outro cenário comum no terreno de “desfazer coisas” é simplesmente descartar mudanças locais quando ainda não foi feito o commit.

Arquivos não estagiados para um Commit

Vamos pressupor que você tenha modificado um arquivo. A execução do comando git status resultaria:

$ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: ola.txt no changes added to commit (use "git add" and/or "git commit -a")

Até aqui nada foi adicionado ao seu repositório Git, nem foi estagiado (registrado) para fazer commit. O que significa descartar aquelas mudanças? Pense na árvore Git. Basta pegar (checkout) a última versão daquele arquivo, certo?

Então:

$ git checkout ola.txt

desfaz a mudança, como o comprova um novo git status:

$ git status On branch master nothing to commit, working directory clean

Arquivos estagiados para um Commit

A outra situação é quando você modificou o arquivo e já o estagiou para dar um commit através do comando git add.

$ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: ola.txt

O comando git checkout não teria nenhum efeito neste caso, mas, ao invés disso, (se você leu o que o git escreveu na saída do parâmetro de status) temos que fazer um reset. Por que? Porque o comando git add já criou um node na árvore Git que, todavia, ainda não foi objeto de um commit (na verdade isso não é 100% correto: veja Git index vs. working tree para mais detalhes). Assim sendo, precisamos resetar o ponteiro corrente para a HEAD que é o topo da nossa branch corrente.

$ git reset HEAD ola.txt Unstaged changes after reset: M ola.txt

e consequentemente:

$ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: ola.txt no changes added to commit (use "git add" and/or "git commit -a")

Estamos novamente no estado em que temos mudanças locais ainda não estagiadas para um commit e podemos então usar o comando checkout para descartá-las. Uma maneira rápida de fazer isso é usar:

$ git reset --hard HEAD

juntando num único comando a retirada de estágio e checkout das mudanças.

Compartilhando/Sincronizando seu Repositório

Ao fim e ao cabo, vamos querer compartilhar nosso código, normalmente sincronizando-o com um repositório central. Para fazer isso, temos que adicionar um remote.

$ git remote add origin git@github.com:user-name/exemplo.git

Para verificar se obteve êxito, basta digitar:

$ git remote -v

o qual lista todos os remotes. Agora é preciso publicar nossa branch master local para o repositório remoto. Isso é feito da seguinte forma:

$ git push -u origin master

E terminamos.

Uma coisa realmente poderosa é que pode-se acrescentar repositórios remotos múltiplos. Isso é usado frequentemente em combinação com soluções de hospedagem em nuvem para distribuição do código no servidor. Por exemplo, você pode acrescentar um remoto chamado deploy que aponta para algum servidor de repositório hospedado na nuvem, tal como:

$ git remote add deploy git@somecloudserver.com:user-name/meuprojeto

e então, sempre que você quiser publicar sua branch, basta executar:

$ git push deploy

Clonagem

Tudo funciona da mesma forma se você pretende iniciar a partir de um repositório remoto já existente. O primeiro passo é fazer um checkout do código-fonte, o que é chamado clonagem (cloning) na terminologia do Git. Deve-se, então, fazer algo assim:

$ git clone git@github.com:user-name/exemplo.git Cloning into 'exemplo'... remote: Counting objects: 430, done. remote: Compressing objects: 100% (293/293), done. remote: Total 430 (delta 184), reused 363 (delta 128) Receiving objects: 100% (430/430), 419.70 KiB | 102 KiB/s, done. Resolving deltas: 100% (184/184), done.

Isso vai criar uma pasta (neste caso) chamada “exemplo” e se entrarmos nela:

$ cd exemplo/

e verificarmos os repositórios remotos, constatamos que a informação para seu rastreamento já está configurada:

$ git remote -v origin git@github.com:juristr/intro.js.git (fetch) origin git@github.com:juristr/intro.js.git (push)

Podemos recomeçar agora o ciclo commit/branch/push normalmente.

Recursos e Links (em inglês)

Os cenários acima são simples, mas provavelmente são também, ao mesmo tempo , os mais usados. O Git, contudo, é capaz de fazer muito mais. Para obter mais detalhes, consulte os links abaixo:

Vídeos

Tutoriais Interativos

Artigo original

Compartilhe esta postagem